Skip to content

Commit 154f693

Browse files
committed
Add a proposal for configuring default actor isolation per module.
1 parent a890bd2 commit 154f693

File tree

1 file changed

+194
-0
lines changed

1 file changed

+194
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# Control default actor isolation inference
2+
3+
* Proposal: [SE-NNNN](NNNN-control-default-actor-isolation.md)
4+
* Authors: [Holly Borla](https://github.com/hborla)
5+
* Review Manager: TBD
6+
* Status: **Awaiting review**
7+
* Vision: [[Prospective Vision] Improving the approachability of data-race safety](https://forums.swift.org/t/prospective-vision-improving-the-approachability-of-data-race-safety/76183)
8+
* Implementation: TBD
9+
* Review: ([pitch](https://forums.swift.org/...))
10+
11+
## Introduction
12+
13+
This proposal introduces a new compiler setting for inferring `@MainActor` isolation by default within the module to mitigate false-positive data-race safety errors in sequential code.
14+
15+
## Motivation
16+
17+
> Note: This motivation section was adapted from the [prospective vision for approachable data-race safety](https://github.com/hborla/swift-evolution/blob/approachable-concurrency-vision/visions/approachable-concurrency.md#mitigating-false-positive-data-race-safety-errors-in-sequential-code). Please see the vision document for extended motivation.
18+
19+
A lot of code is effectively “single-threaded”. For example, most executables, such as apps, command-line tools, and scripts, start running on the main actor and stay there unless some part of the code does something concurrent (like creating a `Task`). If there isn’t any use of concurrency, the entire program will run sequentially, and there’s no risk of data races — every concurrency diagnostic is necessarily a false positive! It would be good to be able to take advantage of that in the language, both to avoid annoying programmers with unnecessary diagnostics and to reinforce progressive disclosure. Many people get into Swift by writing these kinds of programs, and if we can avoid needing to teach them about concurrency straight away, we’ll make the language much more approachable.
20+
21+
The easiest and best way to model single-threaded code is with a global actor. Everything on a global actor runs sequentially, and code that isn’t isolated to that actor can’t access the data that is. All programs start running on the global actor `MainActor`, and if everything in the program is isolated to the main actor, there shouldn’t be any concurrency errors.
22+
23+
Unfortunately, it’s not quite that simple right now. Writing a single-threaded program is surprisingly difficult under the Swift 6 language mode. This is because Swift 6 defaults to a presumption of concurrency: if a function or type is not annotated or inferred to be isolated, it is treated as non-isolated, meaning it can be used concurrently. This default often leads to conflicts with single-threaded code, producing false positive diagnostics in cases such as:
24+
25+
- global and static variables,
26+
- conformances of main-actor-isolated types to non-isolated protocols,
27+
- class deinitializers,
28+
- overrides of non-isolated superclass methods in a main-actor-isolated subclass, and
29+
- calls to main-actor-isolated functions from the platform SDK.
30+
31+
## Proposed solution
32+
33+
This proposal allows code to opt in to being “single-threaded” by default, on a module-by-module basis. A new `-default-isolation` compiler flag specifies the default isolation within the module, and a corresponding `SwiftSetting` method specifies the default isolation per target within a Swift package.
34+
35+
This would change the default isolation rule for unannotated code in the module: rather than being non-isolated, and therefore having to deal with the presumption of concurrency, the code would instead be implicitly isolated to `@MainActor`. Code imported from other modules would be unaffected by the current module’s choice of default. When the programmer really wants concurrency, they can request it explicitly by marking a function or type as `nonisolated` (which can used on any declaration as of [SE-0449](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0449-nonisolated-for-global-actor-cutoff.md)), or they can define it in a module that doesn’t default to main-actor isolation.
36+
37+
## Detailed design
38+
39+
### Specifying default isolation per module
40+
41+
#### `-default-isolation` compiler flag
42+
43+
The `-default-isolation` flag can be used to control the default actor isolation for all code in the module. The only valid arguments to `-default-isolation` are `MainActor` and `nonisolated`. It is an error to specify both `-default-isolation MainActor` and `-default-isolation nonisolated`. If no `-default-isolation` flag is specified, the default isolation for the module is `nonisolated`.
44+
45+
#### `SwiftSetting.defaultIsolation` method
46+
47+
The following method on `SwiftSetting` can be used to specify the default actor isolation per target in a Swift package manifest:
48+
49+
```swift
50+
extension SwiftSetting {
51+
@available(_PackageDescription, introduced: 6.2)
52+
public static func defaultIsolation(
53+
_ globalActor: MainActor.Type?,
54+
_ condition: BuildSettingCondition? = nil
55+
) -> SwiftSetting
56+
}
57+
```
58+
59+
The only valid values for the `globalActor` argument are `MainActor.self` and `nil`.
60+
61+
### Default actor isolation inference
62+
63+
When the default actor isolation is specified as `MainActor`, declarations are inferred to be `@MainActor`-isolated by default. Default isolation does not apply in the following cases:
64+
65+
* Declarations with explicit actor isolation
66+
* Declarations with inferred actor isolation from a superclass, overridden method, protocol conformance, or member propagation
67+
* Static variables and methods in an actor
68+
* Actor initializers and deinitializers
69+
* Declarations that cannot have global actor isolation, including typealiases, import statements, enum cases, and individual accessors
70+
71+
The following code example shows the inferred actor isolation in comments given the code is built with `-default-isolation MainActor`:
72+
73+
```swift
74+
// @MainActor
75+
func f() {}
76+
77+
// @MainActor
78+
class C {
79+
// @MainActor
80+
init() { ... }
81+
82+
// @MainActor
83+
deinit { ... }
84+
85+
// @MainActor
86+
struct Nested { ... }
87+
88+
// @MainActor
89+
static var value = 10
90+
}
91+
92+
@globalActor
93+
actor MyActor {
94+
// nonisolated
95+
init() { ... }
96+
97+
// nonisolated
98+
deinit { ... }
99+
100+
// nonisolated
101+
static let shared = MyActor()
102+
}
103+
104+
@MyActor
105+
protocol P {}
106+
107+
// @MyActor
108+
struct S: P {
109+
// @MyActor
110+
func f() { ... }
111+
}
112+
```
113+
114+
This proposal does not change the default isolation inference rules for closures. Non-Sendable closures and closures passed to `Task.init` already have the same isolation as the enclosing context by default. When specifying `MainActor` isolation by default in a module, non-`@Sendable` closures and `Task.init` closures will have inferred `@MainActor` isolation when the default `@MainActor` inference rules apply to the enclosing context:
115+
116+
```swift
117+
// Built with -default-isolation MainActor
118+
119+
// @MainActor
120+
func f() {
121+
Task { // @MainActor in
122+
...
123+
}
124+
125+
Task.detached { // nonisolated in
126+
...
127+
}
128+
}
129+
130+
nonisolated func g() {
131+
Task { // nonisolated in
132+
...
133+
}
134+
}
135+
```
136+
137+
## Source compatibility
138+
139+
Changing the default actor isolation for a given module or source file is a source incompatible change. The default isolation will remain the same for existing projects unless they explicitly opt into `@MainActor` inference by default via `-default-isolation MainActor` or `defaultIsolation(MainActor.self)` in a package manifest.
140+
141+
## ABI compatibility
142+
143+
This proposal has no ABI impact on existing code.
144+
145+
## Implications on adoption
146+
147+
This proposal does not change the adoption implications of adding `@MainActor` to a declaration that was previously `nonisolated` and vice versa. The source and ABI compatibility implications of changing actor isolation are documented in the Swift migration guide's [Library Evolution](https://github.com/apple/swift-migration-guide/blob/29d6e889e3bd43c42fe38a5c3f612141c7cefdf7/Guide.docc/LibraryEvolution.md#main-actor-annotations) article.
148+
149+
## Future directions
150+
151+
### Specify build settings per file
152+
153+
There are some build settings that are applicable on a per-file bases, including specifying default actor isolation and controlling diagnostic behavior. We can allow those settings to be configured in individual files, using the `SwiftSetting` APIs for a natural way to configure build settings in source code:
154+
155+
```swift
156+
setting [
157+
.defaultIsolation(MainActor.self),
158+
.enableUpcomingFeature("ExistentialAny")
159+
]
160+
161+
// implicitly @MainActor-isolated
162+
protocol P {}
163+
164+
// implicitly @MainActor-isolated
165+
func test(a: P) {} // warning: must be written 'any P'
166+
```
167+
168+
This would be limited to build settings that make sense on a per-file bases. For example, `-enable-library-evolution` must be set for the entire module and cannot be set per file. The strawman syntax used above introduces a new top-level `setting` statement that is applied to an array of `SwiftSetting`s. The `SwiftSetting` API is used for a consistent way to specify build settings in source code.
169+
170+
Note that I plan to pitch per-file build settings imminently because I think it's an important capability, but I believe the design discussion (including syntax bike-shedding!) is separable from this proposal to control default actor isolation.
171+
172+
## Alternatives considered
173+
174+
### Allow defaulting isolation to a custom global actor
175+
176+
The `-default-isolation` flag could allow a custom global actor as the argument, and the `SwiftSetting` API could be updated to accept `(any GlobalActor.Type)?`:
177+
178+
```swift
179+
extension SwiftSetting {
180+
@available(_PackageDescription, introduced: 6.2)
181+
public static func defaultIsolation(
182+
_ globalActor: (any GlobalActor.Type)?,
183+
_ condition: BuildSettingCondition? = nil
184+
) -> SwiftSetting
185+
}
186+
187+
SwiftSetting.defaultIsolation(MyGlobalActor.self)
188+
```
189+
190+
This proposal only supports `MainActor` because any other global actor does not help with progressive disclosure. It has the opposite effect - it forces asynchrony on any main-actor-isolated caller. However, there's nothing in this proposal prohibits generalizing these settings to supporting arbitrary global actors in the future if a compelling use case arises.
191+
192+
## Acknowledgments
193+
194+
Thank you to John McCall for providing much of the motivation for this pitch in the approachable data-race safety vision document, and to Michael Gottesman for helping with the implementation.

0 commit comments

Comments
 (0)