Skip to content

Commit b432080

Browse files
authored
Merge pull request #2560 from simanerush/nonisolated-for-global-actor-cutoff
Add a proposal to allow `nonisolated` to prevent global actor inference.
2 parents 3a7ff03 + e76147d commit b432080

File tree

1 file changed

+383
-0
lines changed

1 file changed

+383
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
1+
# Allow `nonisolated` to prevent global actor inference
2+
3+
* Proposal: [SE-0449](0449-nonisolated-for-global-actor-cutoff.md)
4+
* Authors: [Sima Nerush](https://github.com/simanerush), [Holly Borla](https://github.com/hborla)
5+
* Review Manager: [Tony Allevato](https://github.com/allevato)
6+
* Status: **Active Review (October 2...16, 2024)**
7+
* Implementation: On `main` gated behind `-enable-experimental-feature GlobalActorInferenceCutoff`
8+
* Review: ([pitch](https://forums.swift.org/t/pitch-allow-nonisolated-to-prevent-global-actor-inference/74502))
9+
10+
## Introduction
11+
12+
This proposal allows annotating a set of declarations with `nonisolated` to prevent global actor inference. Additionally, it extends the existing rules for when `nonisolated` can be written on a stored property, improving usability.
13+
14+
## Motivation
15+
16+
Global actor inference has a number of different inference sources. For example, a global actor may be inferred on a type that conforms to a protocol because the protocol is annotated with a global actor attribute:
17+
18+
```swift
19+
@MainActor
20+
protocol GloballyIsolated {}
21+
22+
struct S: GloballyIsolated {} // implicitly globally-isolated
23+
```
24+
25+
In the above code, the struct `S` is inferring the global actor isolation from the explicitly globally-isolated protocol `GloballyIsolated` which it conforms to. While this code is straightforward, the conformance list can quickly get long, and global actor isolation can be inferred through a chain of protocol refinements or superclasses. It can become difficult for a programmer to understand where the global isolation is being inferred from on a given type.
26+
27+
While it is safe for a type with nonisolated methods to conform to a protocol marked with a global actor attribute, sometimes the programmer may want their type to be nonisolated. However, it is challenging to stop global actor inference from happening altogether. Programmers can annotate individual functions with the `nonisolated` keyword, but there is no straightforward way to prevent global actor inference on a type.
28+
29+
Currently, there are two common ways a programmer can “cut-off” the global actor inference from happening on a type when inference comes from a conformance. The first way is to conform to a protocol that causes global isolation to be inferred in an extension, and then marking all of its required properties and methods as `nonisolated`:
30+
31+
```swift
32+
@MainActor
33+
protocol P {
34+
var x: Int { get }
35+
}
36+
37+
struct S {}
38+
39+
extension S: P {
40+
nonisolated var x: Int {
41+
get { 1 }
42+
}
43+
nonisolated func test() {
44+
print(x)
45+
}
46+
}
47+
```
48+
49+
In the above code, `S` can still conform to the globally-isolated protocol `P` without inferring the isolation, but this comes at a cost of the programmer having to manually annotate each protocol requirement with `nonisolated`.
50+
51+
However, the above method would not work for cutting off the global isolation inference on a protocol itself. There is a very nonobvious workaround: when the compiler is inferring global actor isolation, if there are multiple inference sources with conflicting global actors, no global actor is inferred. This is demonstrated by the following example:
52+
53+
```swift
54+
final class FakeExecutor: SerialExecutor {
55+
static let shared: FakeExecutor = .init()
56+
57+
func enqueue(_ job: consuming ExecutorJob) {
58+
fatalError()
59+
}
60+
}
61+
62+
@globalActor
63+
public actor FakeGlobalActor: Sendable {
64+
public static var shared = FakeGlobalActor()
65+
66+
private init() {}
67+
public nonisolated var unownedExecutor: UnownedSerialExecutor {
68+
FakeExecutor.shared.asUnownedSerialExecutor()
69+
}
70+
}
71+
72+
@MainActor
73+
protocol GloballyIsolated {}
74+
75+
@FakeGlobalActor
76+
protocol RemoveGlobalActor {}
77+
78+
protocol RefinedProtocol: GloballyIsolated, RemoveGlobalActor {} // 'RefinedProtocol' is non-isolated
79+
```
80+
81+
In the above code, the programmer creates a new protocol that is isolated to an actor that nominally is isolated to the global actor. This means that the protocol declaration `RefinedProtocol` refining the `RemoveGlobalActor` protocol will result in a conflicting global actor isolation, one from `GloballyIsolated` that’s isolated to `@MainActor`, and another one from `RemoveGlobalActor` that’s isolated to the `@FakeGlobalActor`. This results in the overall declaration having no global actor isolation, while still refining the protocols it conformed to.
82+
83+
84+
## Proposed solution
85+
86+
We propose to allow explicitly writing `nonisolated` on all type and protocol declarations for opting out of the global isolation inference:
87+
88+
```swift
89+
nonisolated struct S: GloballyIsolated, NonIsolatedProto {} // 'S' won't inherit isolation from 'GloballyIsolated' protocol
90+
```
91+
92+
In the above code, the programmer cuts off the global actor inference coming from the `GloballyIsolated` protocol for the struct `S`. Now, the workaround where the programmer had to write an additional protocol with global actor isolation is no longer needed.
93+
94+
```swift
95+
nonisolated protocol P: GloballyIsolated {} // 'P' won't inherit isolation of 'GloballyIsolated' protocol
96+
```
97+
98+
And in the above code, the protocol `P` refines the `GloballyIsolated` protocol. Because `nonisolated` is applied to it, the global actor isolation coming from the `GloballyIsolated` protocol will not be inferred for protocol `P`.
99+
100+
In addition to the above, we propose extending existing rules for when `nonisolated` can be applied to stored properties to improve usability. More precisely, we propose `nonisolated` inference from within the module for mutable storage of `Sendable` value types, and annotating such storage with `nonisolated` to allow synchronous access from outside the module. Additionally, we propose explicit spelling of `nonisolated` for stored properties of non-`Sendable` types.
101+
102+
## Detailed design
103+
104+
Today, there are a number of places where `nonisolated` can be written, as proposed in [SE-0313: Improved control over actor isolation](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0313-actor-isolation-control.md#non-isolated-declarations):
105+
106+
* Functions
107+
* Stored properties of classes that are `let` and `Sendable`
108+
109+
Additionally, under [SE-0434: Usability of global-actor-isolated types](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0434-global-actor-isolated-types-usability.md), `nonisolated` is allowed to be written on mutable `Sendable` storage of globally-isolated value types.
110+
111+
In this proposal, we expand the above rules by allowing annotating more declarations with `nonisolated`. The first batch of these rules is specifically targeting the global actor inference "cut-off", while the second focuses on usability improvements allowing `nonisolated` to be written on more kinds of storage.
112+
113+
### Allowing `nonisolated` to prevent global actor inference
114+
115+
#### Protocols
116+
117+
This proposal allows `nonisolated` attribute to be applied on protocol declarations:
118+
119+
```swift
120+
nonisolated protocol Refined: GloballyIsolated {}
121+
122+
struct A: Refined {
123+
var x: NonSendable
124+
nonisolated func printX() {
125+
print(x) // okay, 'x' is non-isolated
126+
}
127+
}
128+
```
129+
130+
In the above code, the protocol `Refined` is refining the `GloballyIsolated` protocol, but is declared non-isolated. This means that the `Refined` still has the same requirements as `GloballyIsolated`, but they are not isolated. Therefore, a struct `A` conforming to it is also non-isolated, which allows the programmer for more flexibility when implementing the requirements of a protocol.
131+
132+
#### Extensions
133+
134+
Today, it is possible for extensions to be globally-isolated:
135+
136+
```swift
137+
struct X {}
138+
139+
@MainActor extension X {
140+
func f() {} // implicitly globally-isolated
141+
var x: Int { get { 1 } } // implicitly globally-isolated
142+
}
143+
```
144+
145+
In the above code, `X` is a non-isolated struct, and extension members
146+
`f()` and `x` are globally-isolated.
147+
148+
However, if `X` was globally-isolated, before this proposal, the only way to stop extension members from inferring the global actor would be to mark every extension member with
149+
`nonisolated`.
150+
151+
This proposal allows for `nonisolated` attribute to be applied on extension declarations:
152+
153+
```swift
154+
nonisolated extension GloballyIsolated {
155+
var x: NonSendable { .init() }
156+
func implicitlyNonisolated() {}
157+
}
158+
159+
struct C: GloballyIsolated {
160+
nonisolated func explicitlyNonisolated() {
161+
let _ = x // okay
162+
implicitlyNonisolated() // okay
163+
}
164+
}
165+
```
166+
167+
In the code above, the `nonisolated` attribute is applied to an extension declaration for a `GloballyIsolated` protocol. When applied to an extension, `nonisolated` applies to all of its members. In this case, `implicitlyNonisolated` method and the computed property `x` are both nonisolated, and therefore are able to be accessed from a nonisolated context in the body of `explicitlyNonisolated` method of a globally-isolated struct `C`.
168+
169+
#### Classes, structs, and enums
170+
171+
Finally, we propose allowing writing `nonisolated` on class, struct and enum declarations:
172+
173+
```swift
174+
nonisolated class K: GloballyIsolated {
175+
var x: NonSendable
176+
init(x: NonSendable) {
177+
self.x = x // okay, 'x' is non-isolated
178+
}
179+
}
180+
181+
nonisolated struct S: GloballyIsolated {
182+
var x: NonSendable
183+
init(x: NonSendable) {
184+
self.x = x // okay, 'x' is non-isolated
185+
}
186+
}
187+
188+
nonisolated enum E: GloballyIsolated {
189+
func implicitlyNonisolated() {}
190+
init() {}
191+
}
192+
193+
struct TestEnum {
194+
nonisolated func call() {
195+
E().implicitlyNonisolated() // okay
196+
}
197+
}
198+
```
199+
200+
In all the above declarations, the `nonisolated` attribute propagates to all of their members, therefore making them accessible from a non-isolated context.
201+
202+
Importantly, types nested inside of explicitly `nonisolated` declarations still infer actor isolation from their own conformance lists:
203+
204+
```swift
205+
nonisolated struct S: GloballyIsolated {
206+
var value: NotSendable // 'value' is not isolated
207+
struct Nested: GloballyIsolated {} // 'Nested' is still @MainActor-isolated
208+
}
209+
```
210+
211+
The above behavior is semantically consistent with the existing rules around global isolation inference for members of a type:
212+
213+
```swift
214+
@MainActor struct S {
215+
var value: NotSendable // globally-isolated
216+
struct Nested {} // 'Nested' is not @MainActor-isolated
217+
}
218+
```
219+
220+
### Annotating more types of storage with `nonisolated`
221+
222+
This section extends the existing rules for when `nonisolated` can be written on a storage of a user-defined type.
223+
224+
#### Stored properties of non-`Sendable` types
225+
226+
Currently, any stored property of a non-`Sendable` type is implicitly treated as non-isolated. This proposal allows for spelling of this behavior:
227+
228+
```swift
229+
class MyClass {
230+
nonisolated var x: NonSendable = NonSendable() // okay
231+
}
232+
```
233+
234+
Because `MyClass` does not conform to `Sendable`, it cannot be accessed from multiple isolation domains at once. Therefore, the compiler guarantees mutually exclusive access to references of `MyClass` instance. The `nonisolated` on methods and properties of non-`Sendable` types can be safely called from any isolation domain because the base instance can only be accessed by one isolation domain at a time. Importantly, `nonisolated` does not impact the number of isolation domains that can reference the `self` value. As long as there is a reference to `self` value in one isolation domain, the `nonisolated` method/property can be safely called from that domain.
235+
236+
#### Mutable `Sendable` storage of `Sendable` value types
237+
238+
For global-actor-isolated value types, [SE-0434: Usability of global-actor-isolated types](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0434-global-actor-isolated-types-usability.md) allows accessing `var` stored properties with `Sendable` type from within the module as `nonisolated`. This proposal extends this rule to **all** `Sendable` value types:
239+
240+
```swift
241+
protocol P {
242+
@MainActor var y: Int { get }
243+
}
244+
245+
struct S: P {
246+
var y: Int // 'nonisolated' is inferred within the module
247+
}
248+
249+
struct F {
250+
nonisolated func getS(_ s: S) {
251+
let x = s.y // okay
252+
}
253+
}
254+
```
255+
256+
In the above code, the value type `S` is implicitly `Sendable` and its protocol requirement stored property `x` is of `Sendable` type `Int`. While the protocol `P` requires `x` to be globally isolated,
257+
under this proposal, the witness `x` is treated as non-isolated within the module.
258+
When `Sendable` value types are passed between isolation domains, each isolation domain has an independent copy of the value. Accessing properties stored on a value type from across isolation domains is safe as long as the stored property type is also `Sendable`. Even if the stored property is a `var`, assigning to the property will not risk a data race, because the assignment cannot have effects on copies in other isolation domains. Therefore, synchronous access of `x` is okay.
259+
260+
Additionally, [SE-0434](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0434-global-actor-isolated-types-usability.md) allows explicitly annotating globally-isolated value types' properties such as `x` in the previous example with `nonisolated` for enabling synchronous access from outside the module. This proposal extends this rule to **all** `Sendable` value types:
261+
262+
```swift
263+
// In Module A
264+
public protocol P {
265+
@MainActor var y: Int { get }
266+
}
267+
268+
public struct S: P {
269+
public nonisolated var y: Int // 'y' is explicitly non-isolated
270+
}
271+
```
272+
273+
```swift
274+
// In Module B
275+
import A
276+
277+
struct F {
278+
nonisolated func getS(_ s: S) {
279+
let x = s.y // okay
280+
}
281+
}
282+
```
283+
284+
In contrast, `y` is still treated as globally-isolated without the explicit
285+
`nonisolated` attribute:
286+
287+
```swift
288+
// In Module A
289+
public protocol P {
290+
@MainActor var y: Int { get }
291+
}
292+
293+
public struct S: P {
294+
public var y: Int // globally-isolated outside of the module
295+
}
296+
```
297+
298+
```swift
299+
// In Module B
300+
import A
301+
302+
struct F {
303+
nonisolated func getS(_ s: S) {
304+
let x = s.y // error: main actor-isolated property 'y' can not be referenced from a nonisolated context
305+
}
306+
}
307+
```
308+
309+
### Restrictions
310+
311+
Additionally, we propose the following set of rules for when the `nonisolated` attribute **cannot** be applied:
312+
313+
#### Along with some other isolation such as a global actor or an isolated parameter:
314+
315+
```swift
316+
@MainActor
317+
nonisolated struct Conflict {} // error: 'struct 'Conflict' has multiple actor-isolation attributes ('nonisolated' and 'MainActor')'
318+
```
319+
320+
The above code is invalid because the `Conflict` struct cannot simultaneously opt-out of isolation and declare one.
321+
322+
#### On a property of a `Sendable` type when the type of the property does not conform to `Sendable`:
323+
324+
```swift
325+
@MainActor
326+
struct InvalidStruct /* implicitly Sendable */ {
327+
nonisolated let x: NonSendable // error: 'nonisolated' can not be applied to variable with non-'Sendable' type 'NonSendable
328+
}
329+
```
330+
331+
In the above code, `InvalidStruct` is `Sendable`, allowing it to be sent across the concurrency domains. The property `x` is of `NonSendable` type, and if declared `nonisolated`, it would be allowed to be accessed from outside the main actor domain that `InvalidStruct` is isolated to, thus contradicting its lack of `Sendable` capability.
332+
333+
#### On a property of a `Sendable` class when the property is a var:
334+
335+
```swift
336+
@MainActor
337+
final class InvalidClass /* implicitly Sendable */ {
338+
nonisolated var test: Int = 1 // error: 'nonisolated' cannot be applied to mutable stored properties
339+
}
340+
```
341+
342+
In this example, `InvalidClass` is a `Sendable` reference type, which allows concurrent synchronous access to `test` since it is `nonisolated`. This introduces a potential data race.
343+
344+
## Source compatibility
345+
346+
None, this is an additive change to the concurrency model.
347+
348+
## ABI compatibility
349+
350+
None, this proposal does not affect any existing inference rules of the concurrency model.
351+
352+
## Implications on adoption
353+
354+
Consider the following code:
355+
356+
```swift
357+
class C: GloballyIsolated {}
358+
```
359+
360+
`C` currently has an implicit conformance to `Sendable` based on `@MainActor`-inference. Let’s consider what happens when `nonisolated` is adopted for `C`:
361+
362+
```swift
363+
nonisolated class C: GloballyIsolated
364+
```
365+
366+
Now, `C` is no longer implicitly `Sendable`, since the global actor inference is cut off. This can break source compatibility for clients who have relied on the `Sendable` capability of `C`.
367+
368+
## Alternatives considered
369+
370+
### Allowing `nonisolated` on individual types and protocols in the conformance list
371+
372+
Allowing `nonisolated` on individual types and protocols in the conformance list would allow the programmer to opt-out of the global isolation inference from just one or more protocols or types:
373+
374+
```swift
375+
@MyActor
376+
protocol MyActorIsolated {}
377+
378+
struct S: nonisolated GloballyIsolated, MyActorIsolated {} // 'S' is isolated to 'MyActor'
379+
```
380+
381+
In the above code, by selectively applying `nonisolated`, the programmer is able to avoid global actor inference happening from just one of these protocols, meaning the struct `S` can retain isolation, in this case, to `MyActor`.
382+
383+
However, this approach is too cumbersome — the programmer is always able to explicitly specify isolation they want on the type. It also becomes harder to opt-out from any inference from happening, as in the extreme case, the `nonisolated` keyword would have to be applied to every single type or protocol in the conformance list.

0 commit comments

Comments
 (0)