Skip to content

Commit a10a7f8

Browse files
committed
Update proposal to include key paths
1 parent 45a89ba commit a10a7f8

File tree

1 file changed

+178
-37
lines changed

1 file changed

+178
-37
lines changed

proposals/nnnn-inferring-senable-for-methods.md

Lines changed: 178 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
# Inferring `@Sendable` for methods
1+
# Inferring `@Sendable` for methods and key path literals
22

33
* Proposal: [SE-NNNN](https://github.com/kavon/swift-evolution/blob/sendable-functions/proposals/NNNN-filename.md)
4-
* Authors: [Angela Laar](https://github.com/angela-laar), [Kavon Farvardin](https://github.com/kavon)
4+
* Authors: [Angela Laar](https://github.com/angela-laar), [Kavon Farvardin](https://github.com/kavon), [Pavel Yaskevich](https://github.com/xedin)
55
* Review Manager: TBD
66
* Status: Awaiting Implementation
77
* Review: ([pitch](https://forums.swift.org/t/pitch-inferring-sendable-for-methods/66565))
88

99
## Introduction
1010

11-
This proposal is focused on a few corner cases in the language surrounding functions as values when using concurrency. The goal is to improve flexibility, simplicity, and ergonomics without significant changes to Swift.
11+
This proposal is focused on a few corner cases in the language surrounding functions as values and key path literals when using concurrency. We propose Sendability should be inferred for partial and unapplied methods. We also propose to lift a Sendability restriction placed on key path literals in [SE-0302](https://github.com/apple/swift-evolution/blob/main/proposals/0302-concurrent-value-and-concurrent-closures.md#key-path-literals) by allowing the developers to control whether key path literal is Sendable or not. The goal is to improve flexibility, simplicity, and ergonomics without significant changes to Swift.
1212

1313
## Motivation
1414

@@ -33,8 +33,7 @@ let unapplied: (T) -> (() -> Void) = S.f
3333
```
3434

3535

36-
Suppose we want to create a generic method that expects an unapplied function method conforming to Sendable as a parameter. We can create a protocol ``P`` that conforms to the `Sendable` protocol and tell our generic function to expect some generic type that conforms to ``P``. We can also use the `@Sendable` attribute, introduced for closures and functions in [SE-302](https://github.com/kavon/swift-evolution/blob/sendable-functions/proposals/0302-concurrent-value-and-concurrent-closures.md), to annotate the closure parameter.
37-
36+
Suppose we want to create a generic method that expects an unapplied function method conforming to Sendable as a parameter. We can create a protocol ``P`` that conforms to the `Sendable` protocol and tell our generic function to expect some generic type that conforms to ``P``. We can also use the `@Sendable` attribute, introduced for closures and functions in [SE-302](https://github.com/kavon/swift-evolution/blob/sendable-functions/proposals/0302-concurrent-value-and-concurrent-closures.md), to annotate the closure parameter.
3837

3938
```
4039
protocol P: Sendable {
@@ -67,15 +66,47 @@ We can work around this by wrapping our unapplied function in a Sendable closure
6766

6867
```
6968
// S.f($0) == S.f()
70-
g({ @Sendable **in** S.f($0) })
69+
g({ @Sendable in S.f($0) })
70+
```
71+
72+
73+
However, this is a lot of churn to get the expected behavior. The compiler should preserve `@Sendable` in the type signature instead.
74+
75+
**Key Paths**
76+
77+
[SE-0302](https://github.com/apple/swift-evolution/blob/main/proposals/0302-concurrent-value-and-concurrent-closures.md#key-path-literals) makes an explicit mention that all key path literals are treated as implicitly `Sendable` which means that they are not allowed to capture any non-`Sendable` values. This behavior is justified when key path values are passed across concurrency domains or otherwise involved in concurrently executed code but is too restrictive for non-concurrency related code.
78+
7179
```
80+
class Info : Hashable {
81+
// some information about the user
82+
}
83+
84+
public struct Entry {}
7285
86+
public struct User {
87+
public subscript(info: Info) -> Entry {
88+
// find entry based on the given info
89+
}
90+
}
7391
74-
This is a lot of churn to get the expected behavior. The compiler should preserve `@Sendable` in the type signature instead.
92+
let entry: KeyPath<User, Entry> = \.[Info()]
93+
```
94+
95+
With sendability checking enabled this example is going to produce the following warning:
96+
97+
```
98+
warning: cannot form key path that captures non-sendable type 'Info'
99+
let entry: KeyPath<User, Entry> = \.[Info()]
100+
^
101+
```
102+
103+
Use of the key path literal is currently being diagnosed because all key path literals should be Sendable. In actuality, this code is concurrency-safe, there are no data races here because key path doesn’t actually cross any isolation boundary. The compiler should instead verify and diagnose situations when key path is actually passed across an isolation boundary otherwise a warning like that would be confusing for the developers unfamiliar with Swift concurrency, might not always be actionable when type is declared in a different module, and goes against the progressive disclosure principle of the language.
75104

76105
## Proposed solution
77106

78-
We propose the compiler should automatically employ `@Sendable` to functions that cannot capture non-Sendable states. This includes partially-applied and unapplied instance methods of `Sendable` types, as well as non-local functions. Additionally, it should be disallowed to utilize `@Sendable` on instance methods of non-`Sendable` types.
107+
We propose the compiler should automatically apply `Sendable` on functions and key paths that cannot capture non-Sendable values. This includes partially-applied and unapplied instance methods of `Sendable` types, as well as non-local functions. Additionally, it should be disallowed to utilize `@Sendable` on instance methods of non-`Sendable` types.
108+
109+
**Functions**
79110

80111
For a function, the `@Sendable` attribute primarily influences the kinds of values that can be captured by the function. But methods of a nominal type do not capture anything but the object instance itself. Semantically, a method can be thought of as being represented by the following functions:
81112

@@ -93,6 +124,7 @@ func NominalType_method_partiallyAppliedTo(_ obj: NominalType) -> ((ArgType) ->
93124
}
94125
return inner
95126
}
127+
96128
// The actual method call
97129
func NominalType_method(_ self: NominalType, _ arg1: ArgType) -> ReturnType {
98130
/* body of method */
@@ -109,40 +141,98 @@ type NominalType : Sendable {
109141
```
110142

111143
For example, by declaring the following type `Sendable`, the partial and unapplied function values of the type would have implied Sendability and the following code would compile with no errors.
144+
112145
```
113146
struct User : Sendable {
114-
func updatePassword (new: String, old:String) -> Bool {
147+
func updatePassword(new: String, old: String) -> Bool {
115148
/* update password*/
116149
return true
117150
}
118151
}
119152
120-
let unapplied: @Sendable (User) -> ((String, String) Bool) = User.updatePassword // no error
153+
let unapplied: @Sendable (User) -> ((String, String) -> Bool) = User.updatePassword // no error
121154
122155
let partial: @Sendable (String, String) -> Bool = User().updatePassword // no error
123156
```
124157

158+
**Key paths**
159+
160+
Key path literals are very similar to functions, their sendability could only be influenced by sendability of the values they capture in their arguments. Instead of requiring key path literals to always be sendable and warning about cases where key path literals capture non-Sendable types, let’s flip that requirement and allow the developers to explicitly state when a key path is required to be Sendable via `& Sendable` type composition and employ type inference to infer sendability in the same fashion as functions when no contextual type is specified. [The key path hierarchy of types is non-Sendable].
161+
162+
Let’s extend our original example type `User` with a new property and a subscript to showcase the change in behavior:
163+
164+
```
165+
struct User {
166+
var name: String
167+
168+
subscript(_ info: Info) -> Entry { ... }
169+
}
170+
```
171+
172+
A key path to reference a property `name` does not capture any non-Sendable types which means the type of such key path literal could either be inferred as `WritableKeyPath<User, String> & Sendable` or stated to have a sendable type via `& Sendable` composition:
173+
174+
```
175+
let name = \User.name // WritableKeyPath<User, String> **& Sendable**
176+
let name: KeyPath<User, String> & Sendable = \.name // 🟢
177+
```
178+
179+
It is also allowed to use `@Sendable` function type and `& Sendable` key path interchangeably:
180+
181+
```
182+
let name: @Sendable (User) -> String = \.name 🟢
183+
```
184+
185+
It is important to note that **under the proposed rule all of the declarations that do not explicitly specify a Sendable requirement alongside key path type are treated as non-Sendable** (see Source Compatibility section for further discussion):
186+
187+
```
188+
let name: KeyPath<User, String> = \.name // 🟢 but key path is **non-Sendable**
189+
```
190+
191+
Since Sendable is a marker protocol is should be possible to adjust all declarations where `& Sendable` is desirable without any ABI impact.
192+
193+
Existing APIs that use key path in their parameter types or default values can add `Sendable` requirement in a non-ABI breaking way by marking existing declarations as @preconcurrency and adding `& Sendable` at appropriate positions:
194+
195+
```
196+
public func getValue<T, U>(_: KeyPath<T, U>) { ... }
197+
```
198+
199+
becomes
125200

201+
```
202+
@preconcurrency public func getValue<T, U>(_: KeyPath<T, U> & Sendable) { ... }
203+
```
204+
205+
Explicit sendability annotation does not override sendability checking and it would still be incorrect to state that the key path literal is Sendable when it captures non-Sendable values:
206+
207+
```
208+
let entry: KeyPath<User, Entry> & Sendable = \.[Info()] 🔴 Info is a non-Sendable type
209+
```
210+
211+
Such `entry` declaration would be diagnosed by the sendability checker:
212+
213+
```
214+
warning: cannot form key path that captures non-sendable type 'Info'
215+
```
126216

127217
## Detailed design
128218

129-
This proposal includes four changes to `Sendable` behavior.
219+
This proposal includes five changes to `Sendable` behavior.
130220

131221
The first two are what we just discussed regarding partial and unapplied methods.
132222

133223
```
134224
struct User : Sendable {
135-
var address
136-
var password
137-
138-
func changeAddress () {/*do work*/ }
225+
var address: String
226+
var password: String
227+
228+
func changeAddress(new: String, old: String) { /*do work*/ }
139229
}
140230
```
141231

142-
1. The inference of `@Sendable` for unapplied references to methods of a Sendable type.
232+
1. The inference of `@Sendable` for unapplied references to methods of a Sendable type.
143233

144234
```
145-
let unapplied : @Sendable (User) ((String, String) Void) = User.changeAddress // no error
235+
let unapplied : @Sendable (User) -> ((String, String) -> Void) = User.changeAddress // no error
146236
```
147237

148238
2. The inference of `@Sendable` for partially-applied methods of a Sendable type.
@@ -151,42 +241,70 @@ let unapplied : @Sendable (User) → ((String, String) → Void) = User.changeAd
151241
let partial : @Sendable (String, String) → Void = User().changeAddress // no error
152242
```
153243

244+
154245
These two rules include partially applied and unapplied static methods but do not include partially applied or unapplied mutable methods. Unapplied references to mutable methods are not allowed in the language because they can lead to undefined behavior. More details about this can be found in [SE-0042](https://github.com/apple/swift-evolution/blob/main/proposals/0042-flatten-method-types.md).
155246

247+
248+
3. A key path literal without non-Sendable type captures is going to be inferred as key path type with a `& Sendable` requirement or a function type with `@Sendable` attribute.
249+
250+
Key path types respect all of the existing sub-typing rules related to Sendable protocol which means a key path that is not marked as Sendable cannot be assigned to a value that is Sendable (same applies to keypath-to-function conversions):
251+
252+
```
253+
let name: KeyPath<User, String> = \.name
254+
let otherName: KeyPath<User, String> & Sendable = \.name 🔴
255+
let nameFn: @Sendable (User) -> String = name 🔴
256+
```
257+
258+
Key path literals are allowed to infer Sendability requirements from the context i.e. when a key path literal is passed as an argument to a parameter that requires a Sendable type:
259+
260+
```
261+
func getValue<T: Sendable>(_: KeyPath<User, T> & Sendable) -> T {}
262+
263+
getValue(name) // 🟢 both parameter & argument match on sendability requirement
264+
getValue(\.name) // 🟢 use of '& Sendable' by the parameter transfers to the key path literal
265+
getValue(\.[NonSendable()]) // 🔴 This is invalid because key path captures a non-Sendable type
266+
267+
func filter<T: Sendable>(_: @Sendable (User) -> T) {}
268+
filter(name) // 🟢 use of @Sendable applies a sendable key path
269+
```
270+
271+
156272
Next is:
157273

158-
3. The inference of `@Sendable` when referencing non-local functions.
274+
4. The inference of `@Sendable` when referencing non-local functions.
159275

160-
Unlike closures, which retain the captured value, global functions can't capture any variables - because global variables are just referenced by the function without any ownership. With this in mind there is no reason not to make these `Sendable` by default. This change will also include static global functions.
276+
Unlike closures, which retain the captured value, global functions can't capture any variables - because global variables are just referenced by the function without any ownership. With this in mind there is no reason not to make these `Sendable` by default.
161277

162278
```
163279
func doWork() -> Int {
164280
` Int.random(in: 1..<42)`
165281
}
166282
167-
Task<Int, Never>.detached(priority: **nil**, operation: doWork) // Converting non-sendable function value to '@Sendable () async -> Void' may introduce data races
283+
Task<Int, Never>.detached(priority: nil, operation: doWork) // Converting non-sendable function value to '@Sendable () async -> Void' may introduce data races
168284
```
169285

170286
Currently, trying to start a `Task` with the global function `doWork` will cause an error complaining that the function is not `Sendable`. This should compile with no issue.
171287

172-
4. Prohibition of marking methods `@Sendable` when the type they belong to is not `@Sendable`.
288+
5. Prohibition of marking methods `@Sendable` when the type they belong to is not `@Sendable`.
289+
173290
```
174-
class C {
175-
var random: Int = 0 // random is mutable so `C` can't be checked sendable
176-
177-
@Sendable func generateN() async -> Int { //error: adding @Sendable to function of non-Senable type prohibited
178-
random = Int.random(in: 1..<100)
179-
return random
180-
}
291+
class C {
292+
var random: Int = 0 // random is mutable so `C` can't be checked sendable
293+
294+
@Sendable func generateN() async -> Int { //error: adding @Sendable to function of non-Sendable type prohibited
295+
random = Int.random(in: 1..<100)
296+
return random
181297
}
298+
}
182299
183-
func test(c: C) { c.generateN() }
300+
func test(x: C) { x.generateN() }
301+
302+
let num = C()
303+
Task.detached {
304+
test(num)
305+
}
306+
test(num) // data-race
184307
185-
let num = C()
186-
Task.detached {
187-
test(num)
188-
}
189-
test(num) // data-race
190308
```
191309

192310
If we move the previous work we wanted to do into a class that stores the random number we generate as a mutable value, we could be introducing a data race by marking the function responsible for this work `@Sendable` . Doing this should be prohibited by the compiler.
@@ -195,12 +313,28 @@ Since `@Sendable` attribute will be automatically determined with this proposal,
195313

196314
## Source compatibility
197315

198-
No impact.
316+
As described in the Proposed Solution section, some of the existing property and variable declarations **without explicit types** could change their type but the impact of the inference change should be very limited. For example, it would only be possible to observe it when a function or key path value which is inferred as Sendable is passed to an API which is overloaded on Sendable capability:
317+
318+
```
319+
func callback(_: @Sendable () -> Void) {}
320+
func callback(_: () -> Void) {}
321+
322+
callback(MyType.f) // if `f` is inferred as @Sendable first `callback` is preferred
323+
324+
func getValue(_: KeyPath<String, Int> & Sendable) {}
325+
func getValue(_: KeyPath<String, Int>) {}
326+
327+
getValue(\.utf8.count) // prefers first overload of `getValue` if key path is `& Sendable`
328+
```
329+
330+
Such calls to `callback` and `getValue` are currently ambiguous but under the proposed rules the type-checker would pick the first overload of `callback` and `getValue` as a solution if `f` is inferred as `@Sendable` and `\String.utf8.count` would be inferred as having a type of `KeyPath<String, Int> & Sendable` instead of just `KeyPath<String, Int>`.
199331

200332
## Effect on ABI stability
201333

202334
When you remove an explicit `@Sendable` from a method, the mangling of that method will change. Since `@Sendable` will now be inferred, if you choose to remove the explicit annotation to "adopt" the inference, you may need to consider the mangling change.
203335

336+
Adding or removing `& Sendable` from type doesn’t have any ABI impact because `Sendable` is a marker protocol that can be added transparently.
337+
204338
## Effect on API resilience
205339

206340
No effect on ABI stability.
@@ -211,11 +345,18 @@ Accessors are not currently allowed to participate with the `@Sendable` system i
211345

212346
## Alternatives Considered
213347

214-
Swift could forbid explicitly marking function declarations with the` @Sendable` attribute, since under this proposal there’s no longer any reason to do this.
348+
Swift could forbid explicitly marking non-local function declarations with the `@Sendable` attribute, since under this proposal there’s no longer any reason to do this.
215349

216350
```
217351
/*@Sendable*/ func alwaysSendable() {}
218352
```
219-
220353
However, since these attributes are allowed today, this would be a source breaking change. Swift 6 could potentially include fix-its to remove `@Sendable` attributes to ease migration, but it’d still be disruptive. The attributes are harmless under this proposal, and they’re still sometimes useful for code that needs to compile with older tools, so we have chosen not to make this change in this proposal. We can consider deprecation at a later time if we find a good reason to do so.
221354

355+
If we do this, nested functions would not be impacted.
356+
357+
```
358+
func outer() {
359+
@Sendable func inner() {} // This would be OK
360+
}
361+
```
362+

0 commit comments

Comments
 (0)