From 16e9bf08a2d0beae6a319bd5c098187af93a1c01 Mon Sep 17 00:00:00 2001 From: Sophia Poirier Date: Wed, 14 Feb 2024 18:00:20 -0800 Subject: [PATCH 01/16] closure isolation control --- proposals/nnnn-closure-isolation.md | 114 ++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 proposals/nnnn-closure-isolation.md diff --git a/proposals/nnnn-closure-isolation.md b/proposals/nnnn-closure-isolation.md new file mode 100644 index 0000000000..d61d778129 --- /dev/null +++ b/proposals/nnnn-closure-isolation.md @@ -0,0 +1,114 @@ +# Closure isolation control + +* Proposal: [SE-NNNN](nnnn-closure-isolation.md) +* Authors: [Sophia Poirier](https://github.com/sophiapoirier), [John McCall](https://github.com/rjmccall) +* Review Manager: TBD +* Implementation: On `main` gated behind `-enable-experimental-feature TODO` +* Previous Proposals: [SE-0313](0313-actor-isolation-control.md), [SE-0316](0316-global-actors.md) +* Review: ([pitch](https://forums.swift.org/TODO)) + +## Introduction + +This proposal provides the ability to explicitly specify actor-isolation or non-isolation of a closure, as well as providing a parameter attribute to guarantee that a closure parameter inherits the isolation of the context. + +## Motivation + +The formal isolation of a closure can be explicitly specified as global actor isolation: + +```swift +Task { @MainActor in + print("global actor isolation") +} +``` + +Without a global actor isolation annotation, actor-isolation or non-isolation of a closure is inferred but cannot be explicitly specified. This proposal enables closures to be fully explicit about all three types of formal isolation: +* `nonisolated` +* global actor +* specific actor value + +Explicit annotation has the benefit of disabling inference rules and the potential that they lead to a formal isolation that is not preferred. For example, there are circumstances where it is beneficial to guarantee that a closure is `nonisolated` therefore knowing that its execution will hop off the current actor. Explicit annotation also offers the ability to identify a mismatch of intention, such as a case where the developer expected `nonisolated` but inference landed on actor-isolated, and the closure is used in an isolated context. With explicit annotation, the developer would receive a diagnostic about a `nonisolated` closure being used in an actor-isolated context which helpfully identifies this mismatch of intention. + +## Proposed solution + +Enable explicit specification of non-isolation by allowing `nonisolated` to be a specifier on a closure: + +```swift +Task { nonisolated in + print("nonisolated") +} +``` + +Enable explicit specification of actor-isolation via an isolated parameter in a closure's capture list by using the `isolated` specifier: + +```swift +actor A { + func isolate() { + Task { [isolated self] in + print("isolated to 'self'") + } + } +} +``` + +Providing a formal replacement of the experimental parameter attribute `@_inheritActorContext` is needed to resolve another area of ambiguity with closure isolation. Its replacement `@inheritsIsolation` changes the behavior so that it unconditionally and implicitly captures the isolation context (as opposed to currently in actor-isolated contexts it being conditional on whether you capture an isolated parameter or isolated capture or actor-isolated function, but guaranteed if the context is isolated to a global actor or `nonisolated`). + +```swift +class Old { + public init(@_inheritActorContext operation: () async) +} + +class New { + public init(@inheritsIsolation operation: () async) +} + +class C { + var value = 0 + + @MainActor + func staticIsolation() { + Old { + value = 1 // closure is MainActor-isolated and therefore okay to access self + } + New { + value = 2 // closure is MainActor-isolated and therefore okay to access self + } + } + + func dynamicIsolation(_ actor: isolated any Actor) { + Old { + // not isolated to actor without explicit capture + } + New { + // isolated to actor through guaranteed implicit capture + } + } +} +``` + +## Detailed design + +An isolated parameter in a capture list must be of actor type, or conform to or imply an actor, potentially optional, and there can only be one isolated parameter captured, following the same rules described in [SE-0313](0313-actor-isolation-control.md#actor-isolated-parameters) for actor-isolated parameters. + +Opting out of `@inheritsIsolation` can be achieved by explicitly annotating the closure argument as `nonisolated`. + +`@_inheritActorContext` is currently used by the `Task` initializer in the standard library which should be updated to use `@inheritsIsolation` instead. + +## Source compatibility + +The language changes are additive and therefore have no implications on source compatibility. The change to `Task.init` in the standard library does have the potential to isolate some closures that previously were inferred to be `nonisolated`. Prior behavior in those cases could be restored, if desired, by explicitly declaring the closure as `nonisolated`. + +## ABI compatibility + +The language change does not add or affect ABI since formal isolation is already part of a closure's type regardless of whether it is explicitly specified. The `Task.init` cahnge does not impact ABI since the function is annotated with `@_alwaysEmitIntoClient` and therefore has no ABI. + +## Implications on adoption + +none + +## Alternatives considered + +TODO + +## Future directions + +TODO From 7e4d54025d647b52d8259dd7bb4b8b3faf69af3d Mon Sep 17 00:00:00 2001 From: Sophia Poirier Date: Mon, 26 Feb 2024 17:16:52 -0800 Subject: [PATCH 02/16] merge Matt Massicotte's "Parameter Actor Inheritance" proposal --- proposals/nnnn-closure-isolation.md | 98 ++++++++++++++++++++++++++--- 1 file changed, 88 insertions(+), 10 deletions(-) diff --git a/proposals/nnnn-closure-isolation.md b/proposals/nnnn-closure-isolation.md index d61d778129..e245cb8670 100644 --- a/proposals/nnnn-closure-isolation.md +++ b/proposals/nnnn-closure-isolation.md @@ -1,15 +1,28 @@ # Closure isolation control * Proposal: [SE-NNNN](nnnn-closure-isolation.md) -* Authors: [Sophia Poirier](https://github.com/sophiapoirier), [John McCall](https://github.com/rjmccall) +* Authors: [Sophia Poirier](https://github.com/sophiapoirier), [Matt Massicotte](https://github.com/mattmassicotte), [John McCall](https://github.com/rjmccall) * Review Manager: TBD +* Status: **Awaiting review** * Implementation: On `main` gated behind `-enable-experimental-feature TODO` * Previous Proposals: [SE-0313](0313-actor-isolation-control.md), [SE-0316](0316-global-actors.md) -* Review: ([pitch](https://forums.swift.org/TODO)) +* Review: ([pitch](https://forums.swift.org/t/isolation-assumptions/69514)) ## Introduction -This proposal provides the ability to explicitly specify actor-isolation or non-isolation of a closure, as well as providing a parameter attribute to guarantee that a closure parameter inherits the isolation of the context. +This proposal provides the ability to explicitly specify actor-isolation or non-isolation of a closure, as well as providing a parameter attribute to guarantee that a closure parameter inherits the isolation of the context. It makes the isolation inheritance rules more uniform while also making a specific concurrency pattern less restrictive. + +## Table of Contents + +* [Introduction](#introduction) +* [Motivation](#motivation) +* [Proposed solution](#proposed-solution) +* [Detailed design](#detailed-design) +* [Source compatibility](#source-compatibility) +* [ABI compatibility](#abi-compatibility) +* [Implications on adoption](#implications-on-adoption) +* [Alternatives considered](#alternatives-considered) +* [Acknowledgments](#acknowledgments) ## Motivation @@ -26,7 +39,26 @@ Without a global actor isolation annotation, actor-isolation or non-isolation of * global actor * specific actor value -Explicit annotation has the benefit of disabling inference rules and the potential that they lead to a formal isolation that is not preferred. For example, there are circumstances where it is beneficial to guarantee that a closure is `nonisolated` therefore knowing that its execution will hop off the current actor. Explicit annotation also offers the ability to identify a mismatch of intention, such as a case where the developer expected `nonisolated` but inference landed on actor-isolated, and the closure is used in an isolated context. With explicit annotation, the developer would receive a diagnostic about a `nonisolated` closure being used in an actor-isolated context which helpfully identifies this mismatch of intention. +Explicit annotation has the benefit of disabling inference rules and the potential that they lead to a formal isolation that is not preferred. For example, there are circumstances where it is beneficial to guarantee that a closure is `nonisolated` therefore knowing that its execution will hop off the current actor. Explicit annotation also offers the ability to identify a mismatch of intention, such as a case where the developer expected `nonisolated` but inference landed on actor-isolated, and the closure is mistakenly used in an isolated context. Using explicit annotation, the developer would receive a diagnostic about a `nonisolated` closure being used in an actor-isolated context which helpfully identifies this mismatch of intention. + +Additionally, there is a difference in how isolation inheritance behaves via the experimental attribute `@_inheritActorContext` (as used by `Task.init`) for isolated parameters vs actor isolation: global actor isolatation is inherited by `Task`'s initializer closure argument, whereas an actor-isolated parameter is not inherited. This makes it challenging to build intuition around how isolation inheritance works. It also makes some kinds of concurrency patterns impossible to use without being overly restrictive. + +```swift +class NonSendableType { + @MainActor + func globalActor() { + Task { + // accessing self okay + } + } + + func isolatedParameter(_ actor: isolated any Actor) { + Task { + // not okay to access actor + } + } +} +``` ## Proposed solution @@ -50,7 +82,7 @@ actor A { } ``` -Providing a formal replacement of the experimental parameter attribute `@_inheritActorContext` is needed to resolve another area of ambiguity with closure isolation. Its replacement `@inheritsIsolation` changes the behavior so that it unconditionally and implicitly captures the isolation context (as opposed to currently in actor-isolated contexts it being conditional on whether you capture an isolated parameter or isolated capture or actor-isolated function, but guaranteed if the context is isolated to a global actor or `nonisolated`). +Provide a formal replacement of the experimental parameter attribute `@_inheritActorContext` to resolve its ambiguity with closure isolation. Its replacement `@inheritsIsolation` changes the behavior so that it unconditionally and implicitly captures the isolation context (as opposed to currently in actor-isolated contexts it being conditional on whether you capture an isolated parameter or isolated capture or actor-isolated function, but guaranteed if the context is isolated to a global actor or `nonisolated`). ```swift class Old { @@ -97,18 +129,64 @@ Opting out of `@inheritsIsolation` can be achieved by explicitly annotating the The language changes are additive and therefore have no implications on source compatibility. The change to `Task.init` in the standard library does have the potential to isolate some closures that previously were inferred to be `nonisolated`. Prior behavior in those cases could be restored, if desired, by explicitly declaring the closure as `nonisolated`. +It is worth noting that this does not affect the isolation semantics for actor-isolated types that make use of isolated parameters. It is currently impossible to access self in these cases, and even with this new inheritance rule that remains true. + +```swift +actor MyActor { + var mutableState = 0 + + func isolatedParameter(_ actor: isolated any Actor) { + self.mutableState += 1 // invalid + + Task { + self.mutableState += 1 // invalid + } + } +} + +@MainActor +class MyClass { + var mutableState = 0 + + func isolatedParameter(_ actor: isolated any Actor) { + self.mutableState += 1 // invalid + + Task { + self.mutableState += 1 // invalid + } + } +} +``` + ## ABI compatibility -The language change does not add or affect ABI since formal isolation is already part of a closure's type regardless of whether it is explicitly specified. The `Task.init` cahnge does not impact ABI since the function is annotated with `@_alwaysEmitIntoClient` and therefore has no ABI. +The language change does not add or affect ABI since formal isolation is already part of a closure's type regardless of whether it is explicitly specified. The `Task.init` change does not impact ABI since the function is annotated with `@_alwaysEmitIntoClient` and therefore has no ABI. ## Implications on adoption -none +This feature can be freely adopted and un-adopted in source code with no deployment constraints and without affecting source or ABI compatibility. ## Alternatives considered -TODO +When this problem was originally brought up, there were several alternatives suggested. + +The most obvious is to just not use `Task` in combination with non-Sendable types in this way. Restructuring the code to avoid needing to rely on isolation inheritance in the first place. + +```swift +class NonSendableType { + private var internalState = 0 + + func doSomeStuff(isolatedTo actor: isolated any Actor) async throws { + try await Task.sleep(for: .seconds(1)) + print(self.internalState) + } +} +``` + +Despite this being a useful pattern, it does not address the underlying inheritance semantic differences. + +There was also discussion about the ability to make synchronous methods on actors. The scope of such a change is much larger than what is covered here and would still not address the underlying differences. -## Future directions +## Acknowledgments -TODO +Thank you to Franz Busch and Aron Lindberg for looking at the underlying problem so closely and suggesting alternatives. Thank you to Holly Borla for helping to clarify the current behavior, as well as suggesting a path forward that resulted in a much simpler and less-invasive change. From 5903e0aa2cfa9bf608e572cad99db776ca5da3ce Mon Sep 17 00:00:00 2001 From: Sophia Poirier Date: Mon, 26 Feb 2024 18:09:31 -0800 Subject: [PATCH 03/16] add Konrad Malawski's discussion about distributed actors --- proposals/nnnn-closure-isolation.md | 56 ++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/proposals/nnnn-closure-isolation.md b/proposals/nnnn-closure-isolation.md index e245cb8670..0837c90599 100644 --- a/proposals/nnnn-closure-isolation.md +++ b/proposals/nnnn-closure-isolation.md @@ -1,7 +1,7 @@ # Closure isolation control * Proposal: [SE-NNNN](nnnn-closure-isolation.md) -* Authors: [Sophia Poirier](https://github.com/sophiapoirier), [Matt Massicotte](https://github.com/mattmassicotte), [John McCall](https://github.com/rjmccall) +* Authors: [Sophia Poirier](https://github.com/sophiapoirier), [Matt Massicotte](https://github.com/mattmassicotte), [Konrad Malawski](https://github.com/ktoso), [John McCall](https://github.com/rjmccall) * Review Manager: TBD * Status: **Awaiting review** * Implementation: On `main` gated behind `-enable-experimental-feature TODO` @@ -17,11 +17,14 @@ This proposal provides the ability to explicitly specify actor-isolation or non- * [Introduction](#introduction) * [Motivation](#motivation) * [Proposed solution](#proposed-solution) + + [Explicit closure isolation](#explicit-closure-isolation) + + [Isolation inheritance](#isolation-inheritance) * [Detailed design](#detailed-design) * [Source compatibility](#source-compatibility) * [ABI compatibility](#abi-compatibility) * [Implications on adoption](#implications-on-adoption) * [Alternatives considered](#alternatives-considered) +* [Future directions](#future-directions) * [Acknowledgments](#acknowledgments) ## Motivation @@ -62,6 +65,8 @@ class NonSendableType { ## Proposed solution +### Explicit closure isolation + Enable explicit specification of non-isolation by allowing `nonisolated` to be a specifier on a closure: ```swift @@ -82,6 +87,36 @@ actor A { } ``` +The same mechanism works with distributed actors, however only statically "known to be local" distributed actors may be promoted to `isolated`. Currently, this is achieved only through an `isolated` distributed actor type, meaning that a task can only be made isolated to a distributed actor if the value already was isolated, like this: + +``` +import Distributed + +distributed actor D { + func isolateSelf() { + // 'self' is isolated + Task { [isolated self] in print("OK") } // OK: self was isolated + } + + nonisolated func bad() { + // 'self' is not isolated + Task { [isolated self] in print("BAD") } // Error: self was not isolated, and may be remote + } +} + +func isolate(d: isolated D) { + Task { [isolated d] in print("OK") } // OK: d was isolated, thus known-to-be-local +} + +func isolate(d: D) { + Task { [isolated d] in print("OK") } // Error: d was not isolated, and may be remote +} +``` + +While it is technically possible to enqueue work on a remote distributed actor reference, the enqueue on such actor will always immediately crash. Because of that, we err on the side of not allowing such illegal code to begin with. Future directions discuss how this can be made more powerful when it is known that an actor is local. Worth reminding is also the `da.whenLocal { isolated da in ... }` API, which allows dynamically recovering an isolated distributed actor reference, after it has dynamically been checked for locality. + +### Isolation inheritance + Provide a formal replacement of the experimental parameter attribute `@_inheritActorContext` to resolve its ambiguity with closure isolation. Its replacement `@inheritsIsolation` changes the behavior so that it unconditionally and implicitly captures the isolation context (as opposed to currently in actor-isolated contexts it being conditional on whether you capture an isolated parameter or isolated capture or actor-isolated function, but guaranteed if the context is isolated to a global actor or `nonisolated`). ```swift @@ -187,6 +222,25 @@ Despite this being a useful pattern, it does not address the underlying inherita There was also discussion about the ability to make synchronous methods on actors. The scope of such a change is much larger than what is covered here and would still not address the underlying differences. +## Future directions + +### "Known to be local" distributed actors and isolation + +Distributed actors have a property that is currently not exposed in the type-system that is "known to be local". If a distributed is known to be local, code may become isolated to it. + +Once the locality of a type is expressed in the type system the following would be possible: + +``` +let worker: local Worker + +// silly example, showcasing isolating on a known-to-be-local distributed actor +func work(item: Item) async { + await Task { [isolated worker] in + worker.work(on: item) + }.value +} +``` + ## Acknowledgments Thank you to Franz Busch and Aron Lindberg for looking at the underlying problem so closely and suggesting alternatives. Thank you to Holly Borla for helping to clarify the current behavior, as well as suggesting a path forward that resulted in a much simpler and less-invasive change. From af48c4be48f6383da3e834f3361c0e3a4f6f6a06 Mon Sep 17 00:00:00 2001 From: Sophia Poirier Date: Tue, 27 Feb 2024 11:38:55 -0800 Subject: [PATCH 04/16] move distributed actor details into "detailed design" section --- proposals/nnnn-closure-isolation.md | 65 +++++++++++++++-------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/proposals/nnnn-closure-isolation.md b/proposals/nnnn-closure-isolation.md index 0837c90599..05bcc3cf4a 100644 --- a/proposals/nnnn-closure-isolation.md +++ b/proposals/nnnn-closure-isolation.md @@ -20,6 +20,7 @@ This proposal provides the ability to explicitly specify actor-isolation or non- + [Explicit closure isolation](#explicit-closure-isolation) + [Isolation inheritance](#isolation-inheritance) * [Detailed design](#detailed-design) + + [Distributed actor isolation](#distributed-actor-isolation) * [Source compatibility](#source-compatibility) * [ABI compatibility](#abi-compatibility) * [Implications on adoption](#implications-on-adoption) @@ -87,34 +88,6 @@ actor A { } ``` -The same mechanism works with distributed actors, however only statically "known to be local" distributed actors may be promoted to `isolated`. Currently, this is achieved only through an `isolated` distributed actor type, meaning that a task can only be made isolated to a distributed actor if the value already was isolated, like this: - -``` -import Distributed - -distributed actor D { - func isolateSelf() { - // 'self' is isolated - Task { [isolated self] in print("OK") } // OK: self was isolated - } - - nonisolated func bad() { - // 'self' is not isolated - Task { [isolated self] in print("BAD") } // Error: self was not isolated, and may be remote - } -} - -func isolate(d: isolated D) { - Task { [isolated d] in print("OK") } // OK: d was isolated, thus known-to-be-local -} - -func isolate(d: D) { - Task { [isolated d] in print("OK") } // Error: d was not isolated, and may be remote -} -``` - -While it is technically possible to enqueue work on a remote distributed actor reference, the enqueue on such actor will always immediately crash. Because of that, we err on the side of not allowing such illegal code to begin with. Future directions discuss how this can be made more powerful when it is known that an actor is local. Worth reminding is also the `da.whenLocal { isolated da in ... }` API, which allows dynamically recovering an isolated distributed actor reference, after it has dynamically been checked for locality. - ### Isolation inheritance Provide a formal replacement of the experimental parameter attribute `@_inheritActorContext` to resolve its ambiguity with closure isolation. Its replacement `@inheritsIsolation` changes the behavior so that it unconditionally and implicitly captures the isolation context (as opposed to currently in actor-isolated contexts it being conditional on whether you capture an isolated parameter or isolated capture or actor-isolated function, but guaranteed if the context is isolated to a global actor or `nonisolated`). @@ -160,6 +133,36 @@ Opting out of `@inheritsIsolation` can be achieved by explicitly annotating the `@_inheritActorContext` is currently used by the `Task` initializer in the standard library which should be updated to use `@inheritsIsolation` instead. +### Distributed actor isolation + +`isolated` capture parameter works with distributed actors, however only statically "known to be local" distributed actors may be promoted to `isolated`. Currently, this is achieved only through an `isolated` distributed actor type, meaning that a task can only be made isolated to a distributed actor if the value already was isolated, like this: + +```swift +import Distributed + +distributed actor D { + func isolateSelf() { + // 'self' is isolated + Task { [isolated self] in print("OK") } // OK: self was isolated + } + + nonisolated func bad() { + // 'self' is not isolated + Task { [isolated self] in print("BAD") } // error: self was not isolated, and may be remote + } +} + +func isolate(d: isolated D) { + Task { [isolated d] in print("OK") } // OK: d was isolated, thus known-to-be-local +} + +func isolate(d: D) { + Task { [isolated d] in print("OK") } // error: d was not isolated, and may be remote +} +``` + +While it is technically possible to enqueue work on a remote distributed actor reference, the enqueue on such actor will always immediately crash. Because of that, we err on the side of disallowing such illegal code. [Future directions](#future-directions) discusses how this can be made more powerful when it is known that an actor is local. It is also worth noting the `da.whenLocal { isolated da in ... }` API which allows dynamically recovering an isolated distributed actor reference after it has dynamically been checked for locality. + ## Source compatibility The language changes are additive and therefore have no implications on source compatibility. The change to `Task.init` in the standard library does have the potential to isolate some closures that previously were inferred to be `nonisolated`. Prior behavior in those cases could be restored, if desired, by explicitly declaring the closure as `nonisolated`. @@ -226,11 +229,11 @@ There was also discussion about the ability to make synchronous methods on actor ### "Known to be local" distributed actors and isolation -Distributed actors have a property that is currently not exposed in the type-system that is "known to be local". If a distributed is known to be local, code may become isolated to it. +Distributed actors have a property that is currently not exposed in the type system that is "known to be local". If a distributed actor is known to be local, code may become isolated to it. -Once the locality of a type is expressed in the type system the following would be possible: +Once the locality of a type is expressed in the type system, the following would become possible: -``` +```swift let worker: local Worker // silly example, showcasing isolating on a known-to-be-local distributed actor From 36388e1f98fcc49deaabc3fc9b1d8e8275c63fe0 Mon Sep 17 00:00:00 2001 From: Sophia Poirier Date: Thu, 29 Feb 2024 15:23:43 -0800 Subject: [PATCH 05/16] rename file to match title --- ...nnn-closure-isolation.md => nnnn-closure-isolation-control.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename proposals/{nnnn-closure-isolation.md => nnnn-closure-isolation-control.md} (100%) diff --git a/proposals/nnnn-closure-isolation.md b/proposals/nnnn-closure-isolation-control.md similarity index 100% rename from proposals/nnnn-closure-isolation.md rename to proposals/nnnn-closure-isolation-control.md From 34e8ac44606f148ca901ead58c2ae07cf717fd70 Mon Sep 17 00:00:00 2001 From: Sophia Poirier Date: Fri, 1 Mar 2024 16:30:04 -0800 Subject: [PATCH 06/16] address ambiguity of a closure parameter named nonisolated --- proposals/nnnn-closure-isolation-control.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/proposals/nnnn-closure-isolation-control.md b/proposals/nnnn-closure-isolation-control.md index 05bcc3cf4a..38daf1a9f3 100644 --- a/proposals/nnnn-closure-isolation-control.md +++ b/proposals/nnnn-closure-isolation-control.md @@ -202,7 +202,11 @@ The language change does not add or affect ABI since formal isolation is already ## Implications on adoption -This feature can be freely adopted and un-adopted in source code with no deployment constraints and without affecting source or ABI compatibility. +It is possible that existing code could have a closure that names a type-inferred parameter `nonisolated`: +```swift +{ nonisolated in print(nonisolated) } +``` +but with this proposed change, `nonisolated` in this case would instead be interpretted as the contextual keyword specifying the formal isolation of the closure. Such code would then result in a compilation error when trying to use a parameter named `nonisolated`. ## Alternatives considered From 1f0d6fc07d78de4b23e190e824665b20101f46e7 Mon Sep 17 00:00:00 2001 From: Sophia Poirier Date: Thu, 7 Mar 2024 19:50:19 -0800 Subject: [PATCH 07/16] add name of experimental feature flag --- proposals/nnnn-closure-isolation-control.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/nnnn-closure-isolation-control.md b/proposals/nnnn-closure-isolation-control.md index 38daf1a9f3..7d308714c4 100644 --- a/proposals/nnnn-closure-isolation-control.md +++ b/proposals/nnnn-closure-isolation-control.md @@ -4,7 +4,7 @@ * Authors: [Sophia Poirier](https://github.com/sophiapoirier), [Matt Massicotte](https://github.com/mattmassicotte), [Konrad Malawski](https://github.com/ktoso), [John McCall](https://github.com/rjmccall) * Review Manager: TBD * Status: **Awaiting review** -* Implementation: On `main` gated behind `-enable-experimental-feature TODO` +* Implementation: On `main` gated behind `-enable-experimental-feature ClosureIsolation` * Previous Proposals: [SE-0313](0313-actor-isolation-control.md), [SE-0316](0316-global-actors.md) * Review: ([pitch](https://forums.swift.org/t/isolation-assumptions/69514)) From 0fb068e2a1584083b0390594c38799a331b576e1 Mon Sep 17 00:00:00 2001 From: Sophia Poirier Date: Mon, 11 Mar 2024 16:15:45 -0700 Subject: [PATCH 08/16] @nonisolated attribute syntax --- proposals/nnnn-closure-isolation-control.md | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/proposals/nnnn-closure-isolation-control.md b/proposals/nnnn-closure-isolation-control.md index 7d308714c4..1442a770cc 100644 --- a/proposals/nnnn-closure-isolation-control.md +++ b/proposals/nnnn-closure-isolation-control.md @@ -68,10 +68,10 @@ class NonSendableType { ### Explicit closure isolation -Enable explicit specification of non-isolation by allowing `nonisolated` to be a specifier on a closure: +Enable explicit specification of non-isolation by allowing `@nonisolated` to be an attribute on a closure: ```swift -Task { nonisolated in +Task { @nonisolated in print("nonisolated") } ``` @@ -129,7 +129,7 @@ class C { An isolated parameter in a capture list must be of actor type, or conform to or imply an actor, potentially optional, and there can only be one isolated parameter captured, following the same rules described in [SE-0313](0313-actor-isolation-control.md#actor-isolated-parameters) for actor-isolated parameters. -Opting out of `@inheritsIsolation` can be achieved by explicitly annotating the closure argument as `nonisolated`. +Opting out of `@inheritsIsolation` can be achieved by explicitly annotating the closure argument as `@nonisolated`. `@_inheritActorContext` is currently used by the `Task` initializer in the standard library which should be updated to use `@inheritsIsolation` instead. @@ -165,7 +165,7 @@ While it is technically possible to enqueue work on a remote distributed actor r ## Source compatibility -The language changes are additive and therefore have no implications on source compatibility. The change to `Task.init` in the standard library does have the potential to isolate some closures that previously were inferred to be `nonisolated`. Prior behavior in those cases could be restored, if desired, by explicitly declaring the closure as `nonisolated`. +The language changes are additive and therefore have no implications on source compatibility. The change to `Task.init` in the standard library does have the potential to isolate some closures that previously were inferred to be `nonisolated`. Prior behavior in those cases could be restored, if desired, by explicitly declaring the closure as `@nonisolated`. It is worth noting that this does not affect the isolation semantics for actor-isolated types that make use of isolated parameters. It is currently impossible to access self in these cases, and even with this new inheritance rule that remains true. @@ -202,17 +202,13 @@ The language change does not add or affect ABI since formal isolation is already ## Implications on adoption -It is possible that existing code could have a closure that names a type-inferred parameter `nonisolated`: -```swift -{ nonisolated in print(nonisolated) } -``` -but with this proposed change, `nonisolated` in this case would instead be interpretted as the contextual keyword specifying the formal isolation of the closure. Such code would then result in a compilation error when trying to use a parameter named `nonisolated`. +none ## Alternatives considered -When this problem was originally brought up, there were several alternatives suggested. +The typical `nonisolated` modifier form (without `@`) was considered, but its ambiguities with type-inferred closure parameters are problematic, particularly disambiguating `{ nonisolated parameter in ... }` as a modifier followed by a single parameter vs both as a bound pair of tokens. -The most obvious is to just not use `Task` in combination with non-Sendable types in this way. Restructuring the code to avoid needing to rely on isolation inheritance in the first place. +One alternative to `@inheritsIsolation` is to not use `Task` in combination with non-Sendable types in this way, restructuring the code to avoid needing to rely on isolation inheritance in the first place. ```swift class NonSendableType { @@ -227,7 +223,7 @@ class NonSendableType { Despite this being a useful pattern, it does not address the underlying inheritance semantic differences. -There was also discussion about the ability to make synchronous methods on actors. The scope of such a change is much larger than what is covered here and would still not address the underlying differences. +There has also been discussion about the ability to make synchronous methods on actors. The scope of such a change is much larger than what is covered here and would still not address the underlying differences. ## Future directions From 0d6fef5a46e288a3abf12a02c5cfeb1306c69152 Mon Sep 17 00:00:00 2001 From: Sophia Poirier Date: Tue, 12 Mar 2024 12:34:46 -0700 Subject: [PATCH 09/16] revert to non-attribute-@ syntax for nonisolated but require parentheses when used with parameters --- proposals/nnnn-closure-isolation-control.md | 22 +++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/proposals/nnnn-closure-isolation-control.md b/proposals/nnnn-closure-isolation-control.md index 1442a770cc..6b5f062274 100644 --- a/proposals/nnnn-closure-isolation-control.md +++ b/proposals/nnnn-closure-isolation-control.md @@ -68,10 +68,10 @@ class NonSendableType { ### Explicit closure isolation -Enable explicit specification of non-isolation by allowing `@nonisolated` to be an attribute on a closure: +Enable explicit specification of non-isolation by allowing `nonisolated` to be a modifier on a closure: ```swift -Task { @nonisolated in +Task { nonisolated in print("nonisolated") } ``` @@ -129,7 +129,13 @@ class C { An isolated parameter in a capture list must be of actor type, or conform to or imply an actor, potentially optional, and there can only be one isolated parameter captured, following the same rules described in [SE-0313](0313-actor-isolation-control.md#actor-isolated-parameters) for actor-isolated parameters. -Opting out of `@inheritsIsolation` can be achieved by explicitly annotating the closure argument as `@nonisolated`. +Due to the ambiguity between the `nonisolated` modifier and a type-inferred closure parameter, most notably disambiguating `{ nonisolated parameter in ... }` as a modifier followed by a single parameter vs both as a bound pair of tokens, the use of parentheses for a parameter list is required when `nonisolated` is specified. + +```swift +{ nonisolated (parameter) in ... } +``` + +Opting out of `@inheritsIsolation` can be achieved by explicitly annotating the closure argument as `nonisolated`. `@_inheritActorContext` is currently used by the `Task` initializer in the standard library which should be updated to use `@inheritsIsolation` instead. @@ -165,7 +171,7 @@ While it is technically possible to enqueue work on a remote distributed actor r ## Source compatibility -The language changes are additive and therefore have no implications on source compatibility. The change to `Task.init` in the standard library does have the potential to isolate some closures that previously were inferred to be `nonisolated`. Prior behavior in those cases could be restored, if desired, by explicitly declaring the closure as `@nonisolated`. +The language changes are additive and therefore have no implications on source compatibility. The change to `Task.init` in the standard library does have the potential to isolate some closures that previously were inferred to be `nonisolated`. Prior behavior in those cases could be restored, if desired, by explicitly declaring the closure as `nonisolated`. It is worth noting that this does not affect the isolation semantics for actor-isolated types that make use of isolated parameters. It is currently impossible to access self in these cases, and even with this new inheritance rule that remains true. @@ -202,11 +208,15 @@ The language change does not add or affect ABI since formal isolation is already ## Implications on adoption -none +It is possible that existing code could have a closure that names a type-inferred parameter `nonisolated`: +```swift +{ nonisolated in print(nonisolated) } +``` +but with this proposed change, `nonisolated` in this case would instead be interpretted as the contextual keyword specifying the formal isolation of the closure. Such code would then result in a compilation error when trying to use a parameter named `nonisolated`. ## Alternatives considered -The typical `nonisolated` modifier form (without `@`) was considered, but its ambiguities with type-inferred closure parameters are problematic, particularly disambiguating `{ nonisolated parameter in ... }` as a modifier followed by a single parameter vs both as a bound pair of tokens. +`@nonisolated` in attribute form was considered to avert the potential for source breakage, but requires an unintuitive inconsistency in the language for when `@` is required vs needs to be avoided. One alternative to `@inheritsIsolation` is to not use `Task` in combination with non-Sendable types in this way, restructuring the code to avoid needing to rely on isolation inheritance in the first place. From d359d1e577d60f3df5d4b0377d31a38c2609ba5a Mon Sep 17 00:00:00 2001 From: Sophia Poirier Date: Tue, 12 Mar 2024 14:43:03 -0700 Subject: [PATCH 10/16] move contents of "Implications on adoption" section into "Source compatibility" section --- proposals/nnnn-closure-isolation-control.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/proposals/nnnn-closure-isolation-control.md b/proposals/nnnn-closure-isolation-control.md index 6b5f062274..0e8eb66493 100644 --- a/proposals/nnnn-closure-isolation-control.md +++ b/proposals/nnnn-closure-isolation-control.md @@ -171,7 +171,13 @@ While it is technically possible to enqueue work on a remote distributed actor r ## Source compatibility -The language changes are additive and therefore have no implications on source compatibility. The change to `Task.init` in the standard library does have the potential to isolate some closures that previously were inferred to be `nonisolated`. Prior behavior in those cases could be restored, if desired, by explicitly declaring the closure as `nonisolated`. +It is possible that existing code could have a closure that names a type-inferred parameter `nonisolated`: +```swift +{ nonisolated in print(nonisolated) } +``` +but with this proposed change, `nonisolated` in this case would instead be interpretted as the contextual keyword specifying the formal isolation of the closure. Such code would then result in a compilation error when trying to use a parameter named `nonisolated`. + +The change to `Task.init` in the standard library does have the potential to isolate some closures that previously were inferred to be `nonisolated`. Prior behavior in those cases could be restored, if desired, by explicitly declaring the closure as `nonisolated`. It is worth noting that this does not affect the isolation semantics for actor-isolated types that make use of isolated parameters. It is currently impossible to access self in these cases, and even with this new inheritance rule that remains true. @@ -208,11 +214,7 @@ The language change does not add or affect ABI since formal isolation is already ## Implications on adoption -It is possible that existing code could have a closure that names a type-inferred parameter `nonisolated`: -```swift -{ nonisolated in print(nonisolated) } -``` -but with this proposed change, `nonisolated` in this case would instead be interpretted as the contextual keyword specifying the formal isolation of the closure. Such code would then result in a compilation error when trying to use a parameter named `nonisolated`. +none ## Alternatives considered From 4d81353fe57efad27cca20dc61a4e6dc55a65ffb Mon Sep 17 00:00:00 2001 From: Sophia Poirier Date: Wed, 13 Mar 2024 14:57:32 -0700 Subject: [PATCH 11/16] Matt Massicotte's corrections and clarifications --- proposals/nnnn-closure-isolation-control.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposals/nnnn-closure-isolation-control.md b/proposals/nnnn-closure-isolation-control.md index 0e8eb66493..c381a2e723 100644 --- a/proposals/nnnn-closure-isolation-control.md +++ b/proposals/nnnn-closure-isolation-control.md @@ -45,7 +45,7 @@ Without a global actor isolation annotation, actor-isolation or non-isolation of Explicit annotation has the benefit of disabling inference rules and the potential that they lead to a formal isolation that is not preferred. For example, there are circumstances where it is beneficial to guarantee that a closure is `nonisolated` therefore knowing that its execution will hop off the current actor. Explicit annotation also offers the ability to identify a mismatch of intention, such as a case where the developer expected `nonisolated` but inference landed on actor-isolated, and the closure is mistakenly used in an isolated context. Using explicit annotation, the developer would receive a diagnostic about a `nonisolated` closure being used in an actor-isolated context which helpfully identifies this mismatch of intention. -Additionally, there is a difference in how isolation inheritance behaves via the experimental attribute `@_inheritActorContext` (as used by `Task.init`) for isolated parameters vs actor isolation: global actor isolatation is inherited by `Task`'s initializer closure argument, whereas an actor-isolated parameter is not inherited. This makes it challenging to build intuition around how isolation inheritance works. It also makes some kinds of concurrency patterns impossible to use without being overly restrictive. +Additionally, there is a difference in how isolation inheritance behaves via the experimental attribute `@_inheritActorContext` (as used by `Task.init`) for isolated parameters vs actor isolation: global actor isolatation is inherited by `Task`'s initializer closure argument, whereas an actor-isolated parameter is not inherited. This makes it challenging to build intuition around how isolation inheritance works. It also makes the concurrency pattern impossible to allow a non-Sendable type to create a new Task that can mutate self. ```swift class NonSendableType { @@ -58,7 +58,7 @@ class NonSendableType { func isolatedParameter(_ actor: isolated any Actor) { Task { - // not okay to access actor + // not okay to access self } } } From 4429af8bc66ae67bff0b49e7e612dbd0208d6788 Mon Sep 17 00:00:00 2001 From: Matt <85322+mattmassicotte@users.noreply.github.com> Date: Fri, 15 Mar 2024 09:30:37 -0400 Subject: [PATCH 12/16] Some typos an a few small wording changes --- proposals/nnnn-closure-isolation-control.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/proposals/nnnn-closure-isolation-control.md b/proposals/nnnn-closure-isolation-control.md index c381a2e723..f137b8416f 100644 --- a/proposals/nnnn-closure-isolation-control.md +++ b/proposals/nnnn-closure-isolation-control.md @@ -10,7 +10,7 @@ ## Introduction -This proposal provides the ability to explicitly specify actor-isolation or non-isolation of a closure, as well as providing a parameter attribute to guarantee that a closure parameter inherits the isolation of the context. It makes the isolation inheritance rules more uniform while also making a specific concurrency pattern less restrictive. +This proposal provides the ability to explicitly specify actor-isolation or non-isolation of a closure, as well as providing a parameter attribute to guarantee that a closure parameter inherits the isolation of the context. It makes the isolation inheritance rules more uniform, helps to better express intention at closure-creation time, and also makes integrating concurrency with non-Sendable types less restrictive. ## Table of Contents @@ -45,7 +45,7 @@ Without a global actor isolation annotation, actor-isolation or non-isolation of Explicit annotation has the benefit of disabling inference rules and the potential that they lead to a formal isolation that is not preferred. For example, there are circumstances where it is beneficial to guarantee that a closure is `nonisolated` therefore knowing that its execution will hop off the current actor. Explicit annotation also offers the ability to identify a mismatch of intention, such as a case where the developer expected `nonisolated` but inference landed on actor-isolated, and the closure is mistakenly used in an isolated context. Using explicit annotation, the developer would receive a diagnostic about a `nonisolated` closure being used in an actor-isolated context which helpfully identifies this mismatch of intention. -Additionally, there is a difference in how isolation inheritance behaves via the experimental attribute `@_inheritActorContext` (as used by `Task.init`) for isolated parameters vs actor isolation: global actor isolatation is inherited by `Task`'s initializer closure argument, whereas an actor-isolated parameter is not inherited. This makes it challenging to build intuition around how isolation inheritance works. It also makes the concurrency pattern impossible to allow a non-Sendable type to create a new Task that can mutate self. +Additionally, there is a difference in how isolation inheritance behaves via the experimental attribute `@_inheritActorContext` (as used by `Task.init`) for isolated parameters vs actor isolation: global actor isolation is inherited by `Task`'s initializer closure argument, whereas an actor-isolated parameter is not inherited. This makes it challenging to build intuition around how isolation inheritance works. It also makes it impossible to allow a non-Sendable type to create a new Task that can access self. ```swift class NonSendableType { @@ -80,7 +80,7 @@ Enable explicit specification of actor-isolation via an isolated parameter in a ```swift actor A { - func isolate() { + nonisolated func isolate() { Task { [isolated self] in print("isolated to 'self'") } @@ -90,7 +90,7 @@ actor A { ### Isolation inheritance -Provide a formal replacement of the experimental parameter attribute `@_inheritActorContext` to resolve its ambiguity with closure isolation. Its replacement `@inheritsIsolation` changes the behavior so that it unconditionally and implicitly captures the isolation context (as opposed to currently in actor-isolated contexts it being conditional on whether you capture an isolated parameter or isolated capture or actor-isolated function, but guaranteed if the context is isolated to a global actor or `nonisolated`). +Provide a formal replacement of the experimental parameter attribute `@_inheritActorContext` to resolve its ambiguity with closure isolation. Currently, `@_inheritActorContext` actual context capture behavior is conditional on whether you capture an isolated parameter or isolated capture or actor-isolated function, but unconditional if the context is isolated to a global actor or `nonisolated`. Its replacement `@inheritsIsolation` changes the behavior so that it unconditionally and implicitly captures the isolation context. ```swift class Old { @@ -167,7 +167,7 @@ func isolate(d: D) { } ``` -While it is technically possible to enqueue work on a remote distributed actor reference, the enqueue on such actor will always immediately crash. Because of that, we err on the side of disallowing such illegal code. [Future directions](#future-directions) discusses how this can be made more powerful when it is known that an actor is local. It is also worth noting the `da.whenLocal { isolated da in ... }` API which allows dynamically recovering an isolated distributed actor reference after it has dynamically been checked for locality. +While it is technically possible to enqueue work on a remote distributed actor reference, the enqueue on such an actor will always immediately crash. Because of that, we err on the side of disallowing such illegal code. [Future directions](#future-directions) discusses how this can be made more powerful when it is known that an actor is local. It is also worth noting the `da.whenLocal { isolated da in ... }` API which allows dynamically recovering an isolated distributed actor reference after it has dynamically been checked for locality. ## Source compatibility @@ -175,7 +175,7 @@ It is possible that existing code could have a closure that names a type-inferre ```swift { nonisolated in print(nonisolated) } ``` -but with this proposed change, `nonisolated` in this case would instead be interpretted as the contextual keyword specifying the formal isolation of the closure. Such code would then result in a compilation error when trying to use a parameter named `nonisolated`. +but with this proposed change, `nonisolated` in this case would instead be interpreted as the contextual keyword specifying the formal isolation of the closure. Such code would then result in a compilation error when trying to use a parameter named `nonisolated`. The change to `Task.init` in the standard library does have the potential to isolate some closures that previously were inferred to be `nonisolated`. Prior behavior in those cases could be restored, if desired, by explicitly declaring the closure as `nonisolated`. From 54edf1daed38c433b6a7c6fb7528593eaf85337e Mon Sep 17 00:00:00 2001 From: Sophia Poirier Date: Tue, 19 Mar 2024 16:23:46 -0700 Subject: [PATCH 13/16] weak isolated in future directions --- proposals/nnnn-closure-isolation-control.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/proposals/nnnn-closure-isolation-control.md b/proposals/nnnn-closure-isolation-control.md index c381a2e723..7e32351a9a 100644 --- a/proposals/nnnn-closure-isolation-control.md +++ b/proposals/nnnn-closure-isolation-control.md @@ -239,6 +239,10 @@ There has also been discussion about the ability to make synchronous methods on ## Future directions +### weak isolated + +Explore support for explicitly `isolated` closure captures to additionally be specified as `weak`. + ### "Known to be local" distributed actors and isolation Distributed actors have a property that is currently not exposed in the type system that is "known to be local". If a distributed actor is known to be local, code may become isolated to it. From 7f25fe93c00c23ebdb14618f5cb79953dfc10abf Mon Sep 17 00:00:00 2001 From: Sophia Poirier Date: Tue, 26 Mar 2024 16:36:14 -0700 Subject: [PATCH 14/16] isolated capture in sync closure --- proposals/nnnn-closure-isolation-control.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/proposals/nnnn-closure-isolation-control.md b/proposals/nnnn-closure-isolation-control.md index 7e32351a9a..1483600b34 100644 --- a/proposals/nnnn-closure-isolation-control.md +++ b/proposals/nnnn-closure-isolation-control.md @@ -129,6 +129,13 @@ class C { An isolated parameter in a capture list must be of actor type, or conform to or imply an actor, potentially optional, and there can only be one isolated parameter captured, following the same rules described in [SE-0313](0313-actor-isolation-control.md#actor-isolated-parameters) for actor-isolated parameters. +The contexts in which an isolated parameter is permitted in the capture list of a synchronous closure are when the closure is: + +* called immediately +* converted to an `async` function type +* converted to an `@isolated(any)` function type +* converted to a non-Sendable function type and has the correct isolation for the context that does the conversion + Due to the ambiguity between the `nonisolated` modifier and a type-inferred closure parameter, most notably disambiguating `{ nonisolated parameter in ... }` as a modifier followed by a single parameter vs both as a bound pair of tokens, the use of parentheses for a parameter list is required when `nonisolated` is specified. ```swift From a0c3109dba95868fee05106bfef54c719018e26b Mon Sep 17 00:00:00 2001 From: Sophia Poirier Date: Wed, 17 Apr 2024 15:10:28 -0700 Subject: [PATCH 15/16] non-@Sendable local functions should inherit their enclosing isolation --- proposals/nnnn-closure-isolation-control.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/proposals/nnnn-closure-isolation-control.md b/proposals/nnnn-closure-isolation-control.md index 5b8b3ccc46..f866132ecb 100644 --- a/proposals/nnnn-closure-isolation-control.md +++ b/proposals/nnnn-closure-isolation-control.md @@ -146,6 +146,8 @@ Opting out of `@inheritsIsolation` can be achieved by explicitly annotating the `@_inheritActorContext` is currently used by the `Task` initializer in the standard library which should be updated to use `@inheritsIsolation` instead. +One further related clarification of isolation inheritence is that non-`@Sendable` local functions should always inherit their enclosing isolation (unless explicitly `nonisolated` or isolated some other way). + ### Distributed actor isolation `isolated` capture parameter works with distributed actors, however only statically "known to be local" distributed actors may be promoted to `isolated`. Currently, this is achieved only through an `isolated` distributed actor type, meaning that a task can only be made isolated to a distributed actor if the value already was isolated, like this: From 25d8171e7dedcfaf0e39a5c9d021a40aa53f95be Mon Sep 17 00:00:00 2001 From: Sophia Poirier Date: Fri, 27 Sep 2024 14:03:24 -0700 Subject: [PATCH 16/16] add missing Void return type on example closure parameters --- proposals/nnnn-closure-isolation-control.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposals/nnnn-closure-isolation-control.md b/proposals/nnnn-closure-isolation-control.md index f866132ecb..43cb509456 100644 --- a/proposals/nnnn-closure-isolation-control.md +++ b/proposals/nnnn-closure-isolation-control.md @@ -94,11 +94,11 @@ Provide a formal replacement of the experimental parameter attribute `@_inheritA ```swift class Old { - public init(@_inheritActorContext operation: () async) + public init(@_inheritActorContext operation: () async -> Void) } class New { - public init(@inheritsIsolation operation: () async) + public init(@inheritsIsolation operation: () async -> Void) } class C {