Skip to content

Commit a50c2c2

Browse files
committed
Update rules for sendable key paths
1 parent a10a7f8 commit a50c2c2

File tree

1 file changed

+86
-21
lines changed

1 file changed

+86
-21
lines changed

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

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Inferring `@Sendable` for methods and key path literals
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)
44
* Authors: [Angela Laar](https://github.com/angela-laar), [Kavon Farvardin](https://github.com/kavon), [Pavel Yaskevich](https://github.com/xedin)
@@ -35,6 +35,7 @@ let unapplied: (T) -> (() -> Void) = S.f
3535

3636
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.
3737

38+
3839
```
3940
protocol P: Sendable {
4041
init()
@@ -104,7 +105,7 @@ Use of the key path literal is currently being diagnosed because all key path li
104105

105106
## Proposed solution
106107

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+
We propose the compiler should automatically employ `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.
108109

109110
**Functions**
110111

@@ -124,7 +125,6 @@ func NominalType_method_partiallyAppliedTo(_ obj: NominalType) -> ((ArgType) ->
124125
}
125126
return inner
126127
}
127-
128128
// The actual method call
129129
func NominalType_method(_ self: NominalType, _ arg1: ArgType) -> ReturnType {
130130
/* body of method */
@@ -144,27 +144,29 @@ For example, by declaring the following type `Sendable`, the partial and unappli
144144

145145
```
146146
struct User : Sendable {
147-
func updatePassword(new: String, old: String) -> Bool {
147+
func updatePassword (new: String, old: String) -> Bool {
148148
/* update password*/
149149
return true
150150
}
151151
}
152152
153-
let unapplied: @Sendable (User) -> ((String, String) -> Bool) = User.updatePassword // no error
153+
let unapplied: @Sendable (User) -> ((String, String) Bool) = User.updatePassword // no error
154154
155155
let partial: @Sendable (String, String) -> Bool = User().updatePassword // no error
156156
```
157157

158158
**Key paths**
159159

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].
160+
Key path literals are very similar to functions, their sendability could be influenced by sendability of the values they capture in their arguments and isolation of the referenced properties and subscripts. 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].
161161

162162
Let’s extend our original example type `User` with a new property and a subscript to showcase the change in behavior:
163163

164164
```
165165
struct User {
166166
var name: String
167167
168+
@MainActor var age: Int
169+
168170
subscript(_ info: Info) -> Entry { ... }
169171
}
170172
```
@@ -214,6 +216,8 @@ Such `entry` declaration would be diagnosed by the sendability checker:
214216
warning: cannot form key path that captures non-sendable type 'Info'
215217
```
216218

219+
In the same fashion key path that references `age` (i.e. `\User.age`), which is a global actor isolated property, is non-Sendable.
220+
217221
## Detailed design
218222

219223
This proposal includes five changes to `Sendable` behavior.
@@ -225,36 +229,69 @@ struct User : Sendable {
225229
var address: String
226230
var password: String
227231
228-
func changeAddress(new: String, old: String) { /*do work*/ }
232+
func changeAddress (new: String, old: String) {/*do work*/ }
229233
}
230234
```
231235

232236
1. The inference of `@Sendable` for unapplied references to methods of a Sendable type.
233237

234238
```
235-
let unapplied : @Sendable (User) -> ((String, String) -> Void) = User.changeAddress // no error
239+
let unapplied : @Sendable (User)-> ((String, String) -> Void) = User.changeAddress // no error
236240
```
237241

238242
2. The inference of `@Sendable` for partially-applied methods of a Sendable type.
239243

240244
```
241-
let partial : @Sendable (String, String) Void = User().changeAddress // no error
245+
`let partial : @Sendable (String, String) -> Void = User().changeAddress // no error`
242246
```
243247

244248

245249
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).
246250

247251

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.
252+
3. A key path literal without non-Sendable type captures and references to actor-isolated properties and/or subscripts is going to be inferred as key path type with a `& Sendable` requirement or a function type with `@Sendable` attribute.
253+
254+
```
255+
extension User {
256+
@MainActor var age: Int { get { 0 } }
257+
}
258+
259+
let ageKP = \User.age
260+
let infoKP = \User.[Info()]
261+
```
262+
263+
The type of age`KP` is `KeyPath<User, Int>` because `age` is isolated to a global actor. Similarly `infoKP` is a non-Sendable key path because `Info()` argument to a subscript reference has a non-Sendable type.
249264

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):
265+
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.
251266

252267
```
253268
let name: KeyPath<User, String> = \.name
254269
let otherName: KeyPath<User, String> & Sendable = \.name 🔴
255-
let nameFn: @Sendable (User) -> String = name 🔴
256270
```
257271

272+
The conversion between key path and a `@Sendable` function doesn’t actually require the key path itself to be `Sendable` because the it’s not captured by the closure but wrapped by it.
273+
274+
```
275+
let name: @Sendable (User) -> String = \.name 🟢
276+
```
277+
278+
The example above is accepted and is transformed by the compiler into:
279+
280+
```
281+
let name: @Sendable (User) -> String = { $0[keyPath: \.name] }
282+
```
283+
284+
But any subscript arguments that are non-Sendable would preclude the conversion because they’d be captured by the implicitly synthesized closure which makes the closure non-Sendable:
285+
286+
```
287+
let value: NonSendable = NonSendable()
288+
let _: @Sendable (User) -> String = \.[value] 🔴
289+
```
290+
291+
This is an error because `value` has a non-Sendable type and the compiler synthesized closure that wraps the key path - `{ $0[keyPath: \.[value]] }` is going to be inferred as non-Sendable (because it captures `value`) hence non-convertible to a `@Sendable` function type.
292+
293+
Similarly if the conversion captures a key path that has a reference to an isolated property or subscript the implicitly generated closure is not inferred to be non-Sendable.
294+
258295
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:
259296

260297
```
@@ -273,7 +310,7 @@ Next is:
273310

274311
4. The inference of `@Sendable` when referencing non-local functions.
275312

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.
313+
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.
277314

278315
```
279316
func doWork() -> Int {
@@ -291,7 +328,7 @@ Currently, trying to start a `Task` with the global function `doWork` will cause
291328
class C {
292329
var random: Int = 0 // random is mutable so `C` can't be checked sendable
293330
294-
@Sendable func generateN() async -> Int { //error: adding @Sendable to function of non-Sendable type prohibited
331+
@Sendable func generateN() async -> Int { //error: adding @Sendable to function of non-Senable type prohibited
295332
random = Int.random(in: 1..<100)
296333
return random
297334
}
@@ -311,6 +348,37 @@ If we move the previous work we wanted to do into a class that stores the random
311348

312349
Since `@Sendable` attribute will be automatically determined with this proposal, you will no longer have to explicitly write it on function and method declarations.
313350

351+
### Extending key path merging functionality to preserve sendability
352+
353+
Existing Key path API provides a way to join two key paths together via using instance method `appending(...)` . Overloads of this method take key path types of varying mutability as their parameters and produce a new “joined” key path of a desired mutability (read-only, writable, or reference writable).
354+
355+
Under the proposed semantics all overloads of this method become non-Sendable but it is possible and desirable to alleviate that and support/propagate sendability if both “base” and “appended” key paths are `Sendable`.
356+
357+
Such could be archived by introducing new overloads to `func appending(...)` that utilize `& Sendable` for their parameter and result in an extension of `Sendable` protocol. For example:
358+
359+
```
360+
extension Sendable where Self: AnyKeyPath {
361+
@inlinable
362+
public func appending<Root, Value, AppendedValue>(
363+
path: KeyPath<Value, AppendedValue> & Sendable
364+
) -> KeyPath<Root, AppendedValue> & Sendable where Self : KeyPath<Root, Value> {
365+
...
366+
}
367+
}
368+
```
369+
370+
This overload would be selected if both “base” key path and the argument are `Sendable` and would produce a new `Sendable` key path:
371+
372+
```
373+
func makeUTF8CountKeyPath<Root>(from base: KeyPath<Root, String> & Sendable) -> KeyPath<Root, Int> & Sendable {
374+
// Both `base` and `\String.utf8.count` are Sendable key paths,
375+
// so `appending(path:)` returns a Sendable key path too.
376+
return base.appending(path: \.utf8.count) 🟢
377+
}
378+
```
379+
380+
Standard library would have to introduce a variety of new overloads to keep `Sendable` capable `appending(...)` on par with existing non-Sendable functionality.
381+
314382
## Source compatibility
315383

316384
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:
@@ -345,18 +413,15 @@ Accessors are not currently allowed to participate with the `@Sendable` system i
345413

346414
## Alternatives Considered
347415

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.
416+
Swift could forbid explicitly marking function declarations with the` @Sendable` attribute, since under this proposal there’s no longer any reason to do this.
349417

350418
```
351419
/*@Sendable*/ func alwaysSendable() {}
352420
```
421+
353422
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.
354423

355-
If we do this, nested functions would not be impacted.
356424

357-
```
358-
func outer() {
359-
@Sendable func inner() {} // This would be OK
360-
}
361-
```
425+
426+
362427

0 commit comments

Comments
 (0)