Skip to content

Commit 2123394

Browse files
committed
auto
1 parent d0f53ca commit 2123394

File tree

1 file changed

+115
-83
lines changed

1 file changed

+115
-83
lines changed

proposals/nnnn-region-based-isolation.md

Lines changed: 115 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -513,15 +513,14 @@ actor Actor {
513513
var field: NonSendable
514514

515515
func method() {
516-
// ns is in a region that is disconnected.
516+
// ns is in a disconnected isolation region.
517517
let ns = NonSendable()
518518
...
519519
}
520520
}
521521

522522
func nonisolatedFunction() async {
523-
// ns is again in a region that is disconnected
524-
// domain.
523+
// ns is in a disconnected isolation region.
525524
let ns = NonSendable()
526525
}
527526

@@ -692,55 +691,6 @@ loosened via the usage of the `disconnected` field attribute. A `disconnected`
692691
field of an actor is a field that is part of a separate isolation region from
693692
the actor. We discuss this an extension below.
694693

695-
### Function Parameters
696-
697-
A function's non-`Sendable` parameters are considered to be part of the same
698-
region due to the region merging rules. If a function is a method or a global
699-
actor isolated function, then the function parameters are considered to be within
700-
an actor isolated region. If a function is a nonisolated async function then the
701-
function parameters are part of a disconnected region with the additional
702-
restriction that the function argument region cannot be transferred. The reason
703-
why they cannot be transferred is that in such a function, we do not know if an
704-
isolation boundary was crossed when our caller called the function:
705-
706-
```swift
707-
// If we were called by isolatedCaller, we would know that x was transferred
708-
// and could transfer it across another isolation boundary. But if we are called
709-
// by nonIsolatedCaller, we can't, so since we don't know our caller we must
710-
// be conservatively correct.
711-
func nonIsolatedCallee(_ x: NonSendable) async { ... }
712-
713-
func nonIsolatedCaller() async {
714-
let x = NonSendable()
715-
716-
// No transfer occurs since callee is also nonisolated.
717-
await nonIsolatedCallee(x)
718-
719-
// So x could be used here.
720-
useValue(x)
721-
}
722-
723-
@MainActor func isolatedCaller() async {
724-
let x = NonSendable()
725-
726-
// A transfer occurs here...
727-
await nonIsolatedCallee(x)
728-
729-
// Error! So x cannot be used here.
730-
useValue(x)
731-
}
732-
```
733-
734-
This restriction follows from this proposal only defining a *transfer*
735-
convention to a callsite if we know that we are crossing an isolation
736-
boundary. Thus we must be conservative and treat parameters as being
737-
non-transferable so we can handle cases where a callee's invocation does not
738-
result in an isolation boundary being crossed. This restriction can be loosened
739-
via the introduction of an explicit function argument convention that binds also
740-
callers called `transferring` that that would cause non-`Sendable` values to be
741-
transferred even when a callee is not an isolation boundary. We discuss this
742-
convention as an extension below.
743-
744694
#### Merging Isolation Regions
745695

746696
Our isolation region rules require us to merge regions when passing two
@@ -822,6 +772,105 @@ our specific kinds of isolation regions:
822772
}
823773
```
824774

775+
776+
### Function Parameters
777+
778+
A function's non-`Sendable` parameters are considered to be part of the same
779+
region due to the region merging rules. If a function is a method or a global
780+
actor isolated function, then the function parameters are considered to be within
781+
an actor isolated region. If a function is a nonisolated async function then the
782+
function parameters are part of a disconnected region with the additional
783+
restriction that the function argument region cannot be transferred. The reason
784+
why they cannot be transferred is that in such a function, we do not know if an
785+
isolation boundary was crossed when our caller called the function:
786+
787+
```swift
788+
// If we were called by isolatedCaller, we would know that x was transferred
789+
// and could transfer it across another isolation boundary. But if we are called
790+
// by nonIsolatedCaller, we can't, so since we don't know our caller we must
791+
// be conservatively correct.
792+
func nonIsolatedCallee(_ x: NonSendable) async { ... }
793+
794+
func nonIsolatedCaller() async {
795+
let x = NonSendable()
796+
797+
// No transfer occurs since callee is also nonisolated.
798+
await nonIsolatedCallee(x)
799+
800+
// So x could be used here.
801+
useValue(x)
802+
}
803+
804+
@MainActor func isolatedCaller() async {
805+
let x = NonSendable()
806+
807+
// A transfer occurs here...
808+
await nonIsolatedCallee(x)
809+
810+
// Error! So x cannot be used here.
811+
useValue(x)
812+
}
813+
```
814+
815+
This restriction follows from this proposal only defining a *transfer*
816+
convention to a callsite if we know that we are crossing an isolation
817+
boundary. Thus we must be conservative and treat parameters as being
818+
non-transferable so we can handle cases where a callee's invocation does not
819+
result in an isolation boundary being crossed. This restriction can be loosened
820+
via the introduction of an explicit function argument convention that binds also
821+
callers called `transferring` that that would cause non-`Sendable` values to be
822+
transferred even when a callee is not an isolation boundary. We discuss this
823+
convention as an extension below.
824+
825+
### `nonisolated` functions, disconnected isolation regions, and async let
826+
827+
When we pass a non-`Sendable` value as an argument to a `nonisolated` function,
828+
the value is only temporarily transferred to the function. This can be seen
829+
since:
830+
831+
* A parameter to a `nonisolated` function cannot be transferred into a different
832+
isolation domain.
833+
* A `nonisolated` function does not have any non-temporary isolated state of its
834+
own that the non-`Sendable` value could escape into.
835+
836+
Despite this temporarility, an actor isolated isolation region can never be
837+
transferred into a `nonisolated` function since the state of the actor is
838+
strongly tied to the actor. But this does mean that a disconnected isolation
839+
region can be used and transferred after the region is transferred to a
840+
`nonisolated` function.
841+
842+
While said property holds for simple function applications, it is not so simple
843+
for async let parameters initialized from a nonisolated function. When we pass a
844+
non-`Sendable` parameter to the nonisolated function, the caller does not know
845+
if the non isolated function has completed running until the async let is
846+
awaited upon. Since we cannot allow for the non-`Sendable` value to be used
847+
concurrently, we must not allow for the value to be used until the async let is
848+
awaited upon:
849+
850+
```swift
851+
let x = NonSendable()
852+
async let y = nonIsolatedFunc(x)
853+
print(x) // Error! x is used while transferred out of current isolation domain
854+
await y
855+
print(x) // Safe since x is no longer being used by nonIsolatedFunc.
856+
```
857+
858+
In contrast, if the async let function was specifically isolated to an actor,
859+
then we have transferred away the value into that other isolation domain and can
860+
no longer use it locally:
861+
862+
```swift
863+
let x = NonSendable()
864+
async let y = transferToMainActor(x)
865+
print(x) // Error! x was already transferred into the main actor!
866+
await y
867+
print(x) // Error! x was already transferred into the main actor!
868+
```
869+
870+
A similar property holds for asynchronous nonisolated coroutines. While the
871+
coroutine is live, we cannot access any disconnected non-`Sendable` parameters
872+
passed to the coroutine.
873+
825874
### non-`Sendable` Closures
826875

827876
Currently non-`Sendable` closures like other non-`Sendable` values are not
@@ -879,14 +928,24 @@ actor Actor {
879928

880929
In contrast, if a closure is nonisolated and only captures non-`Sendable` values
881930
from a disconnected region, then the resulting region from the closures
882-
formation is a disconnected isolation region.
931+
formation is a disconnected isolation region:
932+
933+
```swift
934+
extension Actor {
935+
let x = NonSendable()
936+
// Regions: [(x)]
937+
let closure: () -> () = { print(x) }
938+
// Regions: [(x, closure)]
939+
// ...
940+
}
941+
```
883942

884-
#### Nonisolated Closures
943+
#### Transferring Nonisolated Closures
885944

886945
A nonisolated non-`Sendable` synchronous or asynchronous closure can be
887946
transferred into another isolation domain if the closure's region is never
888-
used again within the closure's defining context:
889-
947+
used again locally:
948+
890949
```swift
891950
extension MyActor {
892951
func synchronousNonIsolatedNonSendableClosure() async {
@@ -965,33 +1024,6 @@ extension Actor {
9651024
}
9661025
```
9671026

968-
### async let and `nonisolated` functions
969-
970-
Swift supports future like functionality via async let. When a non-`Sendable`
971-
type is passed to a `nonisolated` function that is bound to an async let, the
972-
value is temporarily transferred outside of the current isolation domain causing
973-
the value to be unavailable for use until the async let is awaited upon:
974-
975-
```swift
976-
let x = NonSendable()
977-
async let y = nonIsolatedFunc(x)
978-
print(x) // Error! x is used while transferred out of current isolation domain
979-
await y
980-
print(x) // Safe since x is no longer being used by nonIsolatedFunc.
981-
```
982-
983-
In contrast, if the async let function was specifically isolated to an actor,
984-
then we have transferred away the value into that other isolation domain and can
985-
no longer use it locally:
986-
987-
```swift
988-
let x = NonSendable()
989-
async let y = transferToMainActor(x)
990-
print(x) // Error! x was already transferred into the main actor!
991-
await y
992-
print(x) // Error! x was already transferred into the main actor!
993-
```
994-
9951027
### Simplifying `nonisolated` initializers and deinitializers via transferring
9961028

9971029
All of the above discussions involved actors in the context of an isolated self

0 commit comments

Comments
 (0)