NSObject-subclassing actor initializers use a distinctive SIL layout:
the `self` parameter is stored into an `alloc_stack` before any field
assignments, and `ref_element_addr` instructions are derived from a
`load_borrow` of that stack slot rather than directly from the function
argument.
The region isolation analysis did not recognise this pattern, so the
`ref_element_addr` for each field was not identified as belonging to the
actor instance. This caused spurious "self-isolated → self-isolated
cross-isolation" errors for virtually every property kind: `var`,
optional `var`, `weak var`, failable initializers, noncopyable struct
and enum fields, and all tuple field variants.
Fix: add `getSelfFunctionArgumentForRefElementAddr`, which walks the
`ref_element_addr → load_borrow → alloc_stack` chain and uses the AST
(`VarDecl::isActorSelf`) to confirm the stack slot is the self box.
When confirmed, the field is given actor-instance isolation with respect
to the self function argument, the same isolation already assigned to
non-Sendable parameters by the nonisolated-sync-actor-init rule. The
two sides therefore agree on the actor instance and no false-positive is
emitted.
rdar://177309273
Previously, a nonisolated(unsafe) field on an actor-isolated type was
inferred as carrying the enclosing type's isolation (e.g., "main
actor-isolated: nonisolated(unsafe)"). The intent was to propagate the
type's isolation to everything in the field's region while simply
suppressing diagnostics on the field access itself, relying on Sema to
have already validated the declaration. This was incorrect: Sema allows
a nonisolated(unsafe) value to be used in any isolation context, so
propagating the enclosing type's isolation could place differently-
isolated values in the same region — violating an RBI invariant.
This became a problem now that RBI checks region-merge compatibility
more aggressively rather than deferring to Sema. When a
nonisolated(unsafe) field's region — still carrying main-actor isolation
— was merged with a differently-isolated region, RBI would emit a
spurious incompatible-region-merge error. For example:
@MainActor
struct MainActorStruct {
nonisolated(unsafe) var field: NonSendableKlass? = nil
@CustomActor var customField: NonSendableKlass? = nil
init() {
mergeValues(field, customField) // incompatible merge error!
}
}
Here `field` was inferred as main-actor-isolated, so merging its region
with the @CustomActor-isolated `customField` region would fail.
Similarly, accessing a nonisolated(unsafe) field from a nonisolated
context would produce errors because the field's region carried its
parent type's actor isolation:
@MainActor struct StructWrapper {
nonisolated(unsafe) var _store: MyObj?
}
struct Box {
var oldStore: MyObj?
func structTest(w: StructWrapper) {
let _ = oldStore === w._store // error from merge
}
}
Fix this by inferring nonisolated(unsafe) fields as disconnected in all
three field-access paths: ref_element_addr (classes), struct_extract,
and struct_element_addr. A disconnected region can merge freely with any
isolation, so the spurious errors are eliminated and we match Sema's
behavior.
Covers classes, final classes, structs, and generic structs — both
value and address access patterns.
NB: Ideally these fields would be treated as effectively Sendable in
RBI. That is a more invasive change touching many SILIsolationInfo::
isSendable call sites and is too risky at this point in the release.
Since treating them as Sendable would only reduce the set of emitted
diagnostics, it can be done as a follow-up without regression.
rdar://175180417
We cannot use spare bits or other overlapping storage layout tricks with fundamentally
address-only enums, and we can take advantage of this to do borrowing switches or other
in-place projections without copying the value. However, for resilient enums, the
implementation may use spare bit packing, but the type must be handled address-only
outside of its defining module, and we didn't have a way to express that with
borrowing switch. Optimization passes have also been running into problems with the
complexity that we were using `unchecked_take_enum_data_addr` sometimes as a pure
operation. This patch splits the instruction into three:
- `unchecked_inplace_enum_data_addr` represents a nondestructive in-place enum
projection. It is only allowed for enums whose projection operation is
nondestructive.
- `unchecked_take_enum_data_addr` represents a destructive enum projection,
invalidating the enum and leaving the payload to be further consumed.
This matches the current instruction's semantics.
- `unchecked_borrow_enum_data_addr` represents a borrowing enum projection.
The instruction takes a second operand for "scratch" space, which the
enum representation may be copied into in order to avoid invalidating the
enum value, so the result is dependent on the lifetime of both the
original enum and the scratch buffer. This allows for borrowing switches
over resilient enums.
`unchecked_borrow_enum_data_addr` is implemented by taking advantage of the
"address-only enums can't do spare bit optimization" property at runtime.
We inspect the operand type's bitwise-borrowability from its metadata. If
the type is bitwise-borrowable, then we are allowed to bitwise-copy the
enum to the scratch space and apply the projection to the scratch space,
preserving the original value. If the type is not bitwise-borrowable, then
we cannot use spare bit optimization in its layout, so we apply the
projection in-place.
Fixes rdar://174952822.
SIL functions should always have actor isolation set, otherwise
it could lead to wrong deductions in optimization passes like
`SendNonSendable` or `OptimizeHopToExecutor`.
This is a first step to move isolation assignment into
`SILFunction::create`.
Specifically, we previously used translateSILMultiAssign with checked_cast_br
and checked_cast_addr_br and relied upon its behavior around injecting an
overriding isolation to work.
This caused a bunch of problems when dealing with isolated conformances
introduced by checked_cast_br and checked_cast_addr_br.
In this commit, I fix these issues by:
1. Making it so that we actually represent in SILIsolationInfo that a phi
argument from a checked_cast_br can have disconnected or an isolated conformance
isolation. We previously just returned an invalid isolation. This worked since
we took advantage of a quirk in translateSILMultiAssign that overrode the
isolation of certain results. This quirk breaks the abstraction boundary that
SILIsolationInfo decides the isolation of elements and RegionAnalysis just
propagates those around. I want to remove this behavior but that needs to occur
in a future commit since I am trying to be more surgical with this change.
2. I changed checked_cast_br and checked_cast_addr_br to not use
translateSILMultiAssign so that I have more control and can avoid the behavior
quirk. This had a nice benefit that we now properly represent for
checked_cast_br that the merge in between the parent value and the cast value
only happens on the success path.
3. I added tests for isolated conformances and casting to give us more
confidence that the behavior is correct. We previously had pretty minimal tests.
4. I added partition op translation and silisolation info tests for these two
casting instructions to increase code coverage. I want to increase it more, but
this is a good first step.
rdar://172941821
I don't have a specific test for this, but it is obvious it needs to be updated in the same way, so I added some SIL code that can be used to explicitly test this.
rdar://172941821
Previously, getForCastConformances would only use the SILFunction information if
the SILFunction was isolated to a global actor. If the SILFunction was isolated
to an actor instance though, it would return that the cast was task-isolated
causing mismerges.
The way this works overall is that the AST and the TypeChecker decide where
these can be created... but once they are created it is the job of
SendNonSendable to make sure they do not escape that isolation domain, meaning
that they should match the isolation domain of the SILFunction where they are
created.
rdar://172941821
Previously, non-Sendable MetatypeInst instructions were always inferred to be
task isolated or task isolated nonisolated(nonsending). This is incorrect in the
case where the metatype is used in an actor-isolated function. The reason why is
that:
1. SIL should never see a non-Sendable metatype created in a place where it is
illegal to create it. It is Sema's job to ensure that such code never hits SIL.
2. SIL's job is to instead be sure that such non-Sendable metatypes never escape
that isolation domain. Clearly in the case of a metatype that is allowed to be
used in an actor isolated function means we must represent the inferred
isolation of the metatype as actor isolated.
This was exposed by code in swiftpm with the rest of this PR.
Previously, we would identify it as actor isolated. I also fixed a few other
issues where we were not able to merge nonisolated(nonsending) with
partial_apply isolated-any parameters. I also added more SIL specific tests that validated this behavior.
This was exposed by code in SwiftPM and was also reported by @ktoso separately.
rdar://171847058
Restructure the isolation info lattice from a linear progression to a
diamond-shaped lattice that properly models merge failures:
Old: Unknown -> Disconnected -> Task -> Actor
New: Disconnected ---> Task ---> Invalid
\--> Actor -/
This cleanup prepares the isolation lattice for better error handling when
incompatible regions are merged.
Previously, it was incorrect and not a true lattice and prevented us from
modeling merge errors in between Task and Actor isolation. We need to model this
so that we can properly emit errors when we suppress the AST from emitting these
errors during flow isolation. We are also going to move these sorts of errors
completely from the AST to Region Analysis to simplify things.
NOTE: This causes us to emit some more "unknown pattern" errors. I updated the
tests with that so in the next series of commits, we can validate they all go away.
The region isolation proposal requires that nonisolated synchronous and
asynchronous initializers for actors behave consistently. Previously,
non-Sendable parameters of nonisolated *async* actor inits were given
actor-instance isolation, but nonisolated *sync* actor inits were not
treated the same way, causing an unnecessary divergence.
Fix this in SILIsolationInfo::get(SILArgument *) by detecting the case
where a SILFunction is a nonisolated, non-async Initializer whose self
type is an actor, and returning actor-instance isolation for the
argument in that case.
While here, hoist repeated calls to fArg->getFunction() and
func->getActorIsolation() into local variables (func / funcIsolation)
and fold the previously nested if-ladders for the allocator and
init-accessor cases into a single shared funcIsolation guard.
This is safe because:
1. The box can never be written to after initialization due to
definite initialization, and that initialization must occur before the box
escapes to another isolation domain.
2. We restrict this to immutable boxes containing Sendable types since otherwise
we could load a non-Sendable value from the box and produce a fresh value that
could escape into multiple isolation domains, potentially allowing unsafe
concurrent writes.
The important use case that this fixes are as follows:
1. Sendable noncopyable nominal types. Since the type is noncopyable, we must
store it into a let box it to capture it in an escaping closure causing us to
previously error:
```swift
func testNoncopyableSendableStructWithEscapingMainActorAsync() {
let x = NoncopyableStructSendable()
let _ = {
escapingAsyncUse { @MainActor in
useValue(x) // Error!
}
}
}
```
2. Simple capture lists of Sendable noncopyable nominal types. When we put the
value into the capture list, we create a new let binding for the value which
would be a let box since the underlying type is noncopyable:
```swift
func testNoncopyableSendableStructWithEscapingMainActorAsyncNormalCapture() {
let x = NoncopyableStructSendable()
let _ = { [x] in
escapingAsyncUse { @MainActor in
useValue(x)
}
}
}
```
Originally developed as part of rdar://166081666, though it turned out to be
independent of that fix.
The pass works by walking functions in the modules looking for mutable alloc_box
that contains a weak variable and is knowably a capture. In such a case, the
pass checks all uses of the alloc_box interprocedurally including through
closures and if provably immutable marks the box and all closure parameters as
being inferred immutable.
This change also then subsequently changes SILIsolationInfo to make it so that
such boxes are considered Sendable in a conservative manner that pattern matches
the weak reference code emission pretty closely.
The reason why I am doing this is that issue #82427 correctly tightened region
isolation checking to catch unsafe concurrent access to mutable shared
state. However, this introduced a regression for a common Swift pattern:
capturing `self` weakly in escaping closures.
The problem occurs because:
1. Weak captures are stored in heap-allocated boxes.
2. By default, these boxes are **mutable** (`var`) even if never written to after initialization
3. Mutable boxes are non-Sendable (they could be unsafely mutated from multiple threads)
4. Region isolation now correctly errors when sending non-Sendable values across isolation boundaries
This breaks code like:
```swift
@MainActor class C {
func test() {
timer { [weak self] in // Captures self in a mutable box
Task { @MainActor in
self?.update() // ERROR: sending mutable box risks data races
}
}
}
}
```
Note how even though `self` is Sendable since it is MainActor-isolated, the *box
containing* the weak reference is not Sendable because it is mutable.
With the change in this commit, we now recognize that the box can safely be
treated as Sendable since we would never write to it.
rdar://166081666
This commit systematically replaces all calls to `SILIsolationInfo::isNonSendableType(type, fn)`
and `SILIsolationInfo::isSendableType(type, fn)` with their value-based equivalents
`SILIsolationInfo::isNonSendable(value)` and `SILIsolationInfo::isSendable(value)`.
This refactoring enables more precise Sendability analysis for captured values
in closures, which is a prerequisite for treating inferred-immutable weak
captures as Sendable, a modification I will be making a subsequent commit.
I made the type-based `isSendableType(type, fn)` methods private to prevent
future misuse. The only place where isSendableType was needed to be used outside
of SILIsolationInfo itself was when checking the fields of a box. Rather than
exposing the API for that one purpose, I added two APIs specifically for that
use case.
We are creating/relying on a contract between the AST and SIL... that SILDeclRef
should accurately describe the method/accessor that a class_method is from. By
doing this we eliminate pattern matching on the AST which ties this code too
tightly to the AST and makes it brittle in the face of AST changes. This also
fixes an issue where we were not handling setters correctly.
I am doing this now since it is natural to fix it along side fixing the
ref_element_addr issue in the previous commit since they are effectively doing
the same thing.
rdar://153207557
The two pieces of code are fundamentally doing the same thing so I can reuse the
code. I am doing the refactoring as a separate change so that it is easier to
review.
Specifically given a nominal type like the following:
```swift
@MainActor
struct Foo {
@CustomActor var ns: NonSendableKlass
}
```
the isolation for ns should be CustomActor not MainActor.
rdar://160603379
The previous commit in this PR exposed that we were not handling this correctly.
Specifically, we incorrectly started to error in SwiftFoundation.
rdar://162629359
Specifically, this code was added because otherwise we would in swift 5 +
strict-concurrency mode emit two warnings, one at the AST level and one at the
SIL level. Once we are in swift-6 mode, this does not happen since we stop
compiling at the AST level since we will emit the AST level diagnostic as an
error.
To do this, we tried to pattern match what the AST was erroring upon and treat
the parameter as disconnected instead of being isolated. Sadly, this resulted in
us treating certain closure cases incorrectly and not emit a diagnostic
(creating a concurrency hole).
Given that this behavior results in a bad diagnostic only to avoid emitting two
diagnostics in a mode which is not going to last forever... it really doesn't
make sense to keep it. We really need a better way to handle these sorts of
issues. Perhaps a special semantic parameter put on the function that squelches
certain errors. But that is something for another day. The specific case it
messes up is:
```
class NonSendable {
func action() async {}
}
@MainActor
final class Foo {
let value = NonSendable()
func perform() {
Task { [value] in
await value.action() // Should emit error but do not.
}
}
}
```
In this case, we think that value is sending... when it isnt and we should emit
an error.
rdar://146378329
We want 'inout sending' parameters to have the semantics that not only are they
disconnected on return from the function but additionally they are guaranteed to
be in their own disconnected region on return. This implies that we must emit
errors when an 'inout sending' parameter or any element that is in the same
region as the current value within an 'inout sending' parameter is
returned. This commit contains a new diagnostic for RegionIsolation that adds
specific logic for detecting and emitting errors in these situations.
To implement this, we introduce 3 new diagnostics with each individual
diagnostic being slightly different to reflect the various ways that this error
can come up in source:
* Returning 'inout sending' directly:
```swift
func returnInOutSendingDirectly(_ x: inout sending NonSendableKlass) -> NonSendableKlass {
return x // expected-warning {{cannot return 'inout sending' parameter 'x' from global function 'returnInOutSendingDirectly'}}
// expected-note @-1 {{returning 'x' risks concurrent access since caller assumes that 'x' and the result of global function 'returnInOutSendingDirectly' can be safely sent to different isolation domains}}
}
```
* Returning a value in the same region as an 'inout sending' parameter. E.x.:
```swift
func returnInOutSendingRegionVar(_ x: inout sending NonSendableKlass) -> NonSendableKlass {
var y = x
y = x
return y // expected-warning {{cannot return 'y' from global function 'returnInOutSendingRegionVar'}}
// expected-note @-1 {{returning 'y' risks concurrent access to 'inout sending' parameter 'x' since the caller assumes that 'x' and the result of global function 'returnInOutSendingRegionVar' can be safely sent to different isolation domains}}
}
```
* Returning the result of a function or computed property that is in the same
region as the 'inout parameter'.
```swift
func returnInOutSendingViaHelper(_ x: inout sending NonSendableKlass) -> NonSendableKlass {
let y = x
return useNonSendableKlassAndReturn(y) // expected-warning {{cannot return result of global function 'useNonSendableKlassAndReturn' from global function 'returnInOutSendingViaHelper'}}
// expected-note @-1 {{returning result of global function 'useNonSendableKlassAndReturn' risks concurrent access to 'inout sending' parameter 'x' since the caller assumes that 'x' and the result of global function 'returnInOutSendingViaHelper' can be safely sent to different isolation domains}}
}
```
Additionally, I had to introduce a specific variant for each of these
diagnostics for cases where due to us being in a method, we are actually in our
caller causing the 'inout sending' parameter to be in the same region as an
actor isolated value:
* Returning 'inout sending' directly:
```swift
extension MyActor {
func returnInOutSendingDirectly(_ x: inout sending NonSendableKlass) -> NonSendableKlass {
return x // expected-warning {{cannot return 'inout sending' parameter 'x' from instance method 'returnInOutSendingDirectly'}}
// expected-note @-1 {{returning 'x' risks concurrent access since caller assumes that 'x' is not actor-isolated and the result of instance method 'returnInOutSendingDirectly' is 'self'-isolated}}
}
}
```
* Returning a value in the same region as an 'inout sending' parameter. E.x.:
```swift
extension MyActor {
func returnInOutSendingRegionLet(_ x: inout sending NonSendableKlass) -> NonSendableKlass {
let y = x
return y // expected-warning {{cannot return 'y' from instance method 'returnInOutSendingRegionLet'}}
// expected-note @-1 {{returning 'y' risks concurrent access to 'inout sending' parameter 'x' since the caller assumes that 'x' is not actor-isolated and the result of instance method 'returnInOutSendingRegionLet' is 'self'-isolated}}
}
}
```
* Returning the result of a function or computed property that is in the same region as the 'inout parameter'.
```swift
extension MyActor {
func returnInOutSendingViaHelper(_ x: inout sending NonSendableKlass) -> NonSendableKlass {
let y = x
return useNonSendableKlassAndReturn(y) // expected-warning {{cannot return result of global function 'useNonSendableKlassAndReturn' from instance method 'returnInOutSendingViaHelper'; this is an error in the Swift 6 language mode}}
// expected-note @-1 {{returning result of global function 'useNonSendableKlassAndReturn' risks concurrent access to 'inout sending' parameter 'x' since the caller assumes that 'x' is not actor-isolated and the result of instance method 'returnInOutSendingViaHelper' is 'self'-isolated}}
}
}
```
To implement this, I used two different approaches depending on whether or not
the returned value was generic or not.
* Concrete
In the case where we had a concrete value, I was able to in simple cases emit
diagnostics based off of the values returned by the return inst. In cases where
we phied together results due to multiple results in the same function, we
determine which of the incoming phied values caused the error by grabbing the
exit partition information of each of the incoming value predecessors and seeing
if an InOutSendingAtFunctionExit would emit an error.
* Generic
In the case of generic code, it is a little more interesting since the result is
a value stored in an our parameter instead of being a value directly returned by
a return inst. To work around this, I use PrunedLiveness to determine the last
values stored into the out parameter in the function to avoid having to do a
full dataflow. Then I take the exit blocks where we assign each of those values
and run the same check as we do in the direct phi case to emit the appropriate
error.
rdar://152454571
Centralize the logic for figuring out the conformances for the various
init_existential* instructions in a SILIsolationInfo static method, and
always go through that when handling "assign" semantics. This way, we
can use CONSTANT_TRANSLATION again for these instructions, or a simpler
decision process between Assign and LookThrough.
The actually undoes a small change made earlier when we stopped looking
through `init_existential_value` instructions. Now we do when there are
no isolated conformances.
Better match the style of SILIsolationInfo by moving the code for determining
SILIsolationInfo from conformances or dynamic casts to existentials into
static `getXYZ` methods on SILIsolationInfo.
Other than adding an assertion regarding disconnected regions, no
intended functionality change.
When we introduce isolation due to a (potential) isolated conformance,
keep track of the protocol to which the conformance could be
introduced. Use this information for two reasons:
1. Downgrade the error to a warning in Swift < 7, because we are newly
diagnosing these
2. Add a note indicating where the isolated conformance could be introduced.
Specifically in terms of printing, if NonisolatedNonsendingByDefault is enabled,
we print out things as nonisolated/task-isolated and @concurrent/@concurrent
task-isolated. If said feature is disabled, we print out things as
nonisolated(nonsending)/nonisolated(nonsending) task-isolated and
nonisolated/task-isolated. This ensures in the default case, diagnostics do not
change and we always print out things to match the expected meaning of
nonisolated depending on the mode.
I also updated the tests as appropriate/added some more tests/added to the
SendNonSendable education notes information about this.
I am doing this so that I can change how we emit the diagnostics just for
SendNonSendable depending on if NonisolatedNonsendingByDefault is enabled
without touching the rest of the compiler.
This does not actually change any of the actual output though.
We were effectively working around this previously at the SIL level. This caused
us not to obey the semantics of the actual evolution proposal. As an example of
this, in the following, x should not be considered main actor isolated:
```swift
nonisolated(nonsending) func useValue<T>(_ t: T) async {}
@MainActor func test() async {
let x = NS()
await useValue(x)
print(x)
}
```
we should just consider this to be a merge and since useValue does not have any
MainActor isolated parameters, x should not be main actor isolated and we should
not emit an error here.
I also fixed a separate issue where we were allowing for parameters of
nonisolated(nonsending) functions to be passed to @concurrent functions. We
cannot allow for this to happen since the nonisolated(nonsending) parameters
/could/ be actor isolated. Of course, we have lost that static information at
this point so we cannot allow for it. Given that we have the actual dynamic
actor isolation information, we could dynamically allow for the parameters to be
passed... but that is something that is speculative and is definitely outside of
the scope of this patch.
rdar://154139237
In this case, what is happening is that in SILGen, we insert implicit
DistributedActor.asLocalActor calls to convert a distributed actor to its local
any Actor typed form. The intention is that the actor parameter and result are
considered the same... but there is nothing at the SIL level to enforce that. In
this commit, I change ActorInstance (the utility that defines actor identity at
a value level) to look through such a call.
I implemented this by just recognizing the decl directly. We already do this in
parts of SILGen, so I don't really see a problem with doing this. It also
provides a nice benefit that we do not have to modify SILFunctionType to
represent this or put a @_semantic attribute on the getter.
NOTE: Generally, Sema prevents us from mixing together different actors. In this
case, Sema does not help us since this call is inserted implicitly by the
distributed actor implementation in SILGen. So this is not a problem in general.
rdar://152436817
Otherwise, we can be inconsistent with isolations returned by other parts of the
code. Previously we were just treating it always as self + nom decl, which is
clearly wrong if a type is not self (e.x.: if it is an isolated parameter).
rdar://135459885
This also fixes a case where we would have inferred the wrong isolation (which
the assert caught). I think we missed this since it only comes up with final
classes.
rdar://142568522
Introduce a new experimental feature StrictSendableMetatypes that stops
treating all metatypes as `Sendable`. Instead, metatypes of generic
parameters and existentials are only considered Sendable if their
corresponding instance types are guaranteed to be Sendable.
Start with enforcing this property within region isolation. Track
metatype creation instructions and put them in the task's isolation
domain, so that transferring them into another isolation domain
produces a diagnostic. As an example:
func f<T: P>(_: T.Type) {
let x: P.Type = T.self
Task.detached {
x.someStaticMethod() // oops, T.Type is not Sendable
}
}