Skip to content

Commit 3879d2c

Browse files
authored
Avoid sendable key paths in dynamic member lookup (#3463)
* Avoid sendable key paths in dynamic member lookup There are a few compiler bugs that prevent us from declaring sendability for key paths where it's needed. First, doing so breaks autocomplete, which really hurts the developer experience: swiftlang/swift#77035 Second, even though recovering autocomplete might be preferable at the cost of safety, there is no safety to begin with right now because sendable diagnostics don't propagate through dynamic member lookup: swiftlang/swift#77105 Because of this, let's only use non-sendable key paths for now, and force cast them under the hood. * fix
1 parent 59fdf28 commit 3879d2c

File tree

5 files changed

+94
-37
lines changed

5 files changed

+94
-37
lines changed

Sources/ComposableArchitecture/Internal/KeyPath+Sendable.swift

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,69 @@
2222
public typealias _SendableCaseKeyPath<Root, Value> = CaseKeyPath<Root, Value>
2323
#endif
2424

25-
@_transparent
26-
func sendableKeyPath(
27-
_ keyPath: AnyKeyPath
28-
) -> _SendableAnyKeyPath {
29-
#if compiler(>=6)
30-
unsafeBitCast(keyPath, to: _SendableAnyKeyPath.self)
31-
#else
32-
keyPath
33-
#endif
25+
// NB: Dynamic member lookup does not currently support sendable key paths and even breaks
26+
// autocomplete.
27+
//
28+
// * https://github.com/swiftlang/swift/issues/77035
29+
// * https://github.com/swiftlang/swift/issues/77105
30+
extension _AppendKeyPath {
31+
@_transparent
32+
func unsafeSendable() -> _SendableAnyKeyPath
33+
where Self == AnyKeyPath {
34+
#if compiler(>=6)
35+
unsafeBitCast(self, to: _SendableAnyKeyPath.self)
36+
#else
37+
self
38+
#endif
39+
}
40+
41+
@_transparent
42+
func unsafeSendable<Root>() -> _SendablePartialKeyPath<Root>
43+
where Self == PartialKeyPath<Root> {
44+
#if compiler(>=6)
45+
unsafeBitCast(self, to: _SendablePartialKeyPath<Root>.self)
46+
#else
47+
self
48+
#endif
49+
}
50+
51+
@_transparent
52+
func unsafeSendable<Root, Value>() -> _SendableKeyPath<Root, Value>
53+
where Self == KeyPath<Root, Value> {
54+
#if compiler(>=6)
55+
unsafeBitCast(self, to: _SendableKeyPath<Root, Value>.self)
56+
#else
57+
self
58+
#endif
59+
}
60+
61+
@_transparent
62+
func unsafeSendable<Root, Value>() -> _SendableWritableKeyPath<Root, Value>
63+
where Self == WritableKeyPath<Root, Value> {
64+
#if compiler(>=6)
65+
unsafeBitCast(self, to: _SendableWritableKeyPath<Root, Value>.self)
66+
#else
67+
self
68+
#endif
69+
}
70+
71+
@_transparent
72+
func unsafeSendable<Root, Value>() -> _SendableReferenceWritableKeyPath<Root, Value>
73+
where Self == ReferenceWritableKeyPath<Root, Value> {
74+
#if compiler(>=6)
75+
unsafeBitCast(self, to: _SendableReferenceWritableKeyPath<Root, Value>.self)
76+
#else
77+
self
78+
#endif
79+
}
80+
81+
@_transparent
82+
func unsafeSendable<Root, Value>() -> _SendableCaseKeyPath<Root, Value>
83+
where Self == CaseKeyPath<Root, Value> {
84+
#if compiler(>=6)
85+
unsafeBitCast(self, to: _SendableCaseKeyPath<Root, Value>.self)
86+
#else
87+
self
88+
#endif
89+
}
3490
}

Sources/ComposableArchitecture/Observation/Binding+Observation.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,12 @@ extension BindableAction where State: ObservableState {
159159

160160
extension Store where State: ObservableState, Action: BindableAction, Action.State == State {
161161
public subscript<Value: Equatable & Sendable>(
162-
dynamicMember keyPath: _SendableWritableKeyPath<State, Value>
162+
dynamicMember keyPath: WritableKeyPath<State, Value>
163163
) -> Value {
164164
get { self.state[keyPath: keyPath] }
165165
set {
166166
BindingLocal.$isActive.withValue(true) {
167-
self.send(.set(keyPath, newValue, isInvalidated: _isInvalidated))
167+
self.send(.set(keyPath.unsafeSendable(), newValue, isInvalidated: _isInvalidated))
168168
}
169169
}
170170
}
@@ -195,12 +195,12 @@ where
195195
Action.ViewAction.State == State
196196
{
197197
public subscript<Value: Equatable & Sendable>(
198-
dynamicMember keyPath: _SendableWritableKeyPath<State, Value>
198+
dynamicMember keyPath: WritableKeyPath<State, Value>
199199
) -> Value {
200200
get { self.state[keyPath: keyPath] }
201201
set {
202202
BindingLocal.$isActive.withValue(true) {
203-
self.send(.view(.set(keyPath, newValue, isInvalidated: _isInvalidated)))
203+
self.send(.view(.set(keyPath.unsafeSendable(), newValue, isInvalidated: _isInvalidated)))
204204
}
205205
}
206206
}

Sources/ComposableArchitecture/Reducer/Reducers/PresentationReducer.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,10 +293,11 @@ extension PresentationAction: CasePathable {
293293
}
294294

295295
public subscript<AppendedAction>(
296-
dynamicMember keyPath: _SendableCaseKeyPath<Action, AppendedAction>
296+
dynamicMember keyPath: CaseKeyPath<Action, AppendedAction>
297297
) -> AnyCasePath<PresentationAction, AppendedAction>
298298
where Action: CasePathable {
299-
AnyCasePath<PresentationAction, AppendedAction>(
299+
let keyPath = keyPath.unsafeSendable()
300+
return AnyCasePath<PresentationAction, AppendedAction>(
300301
embed: { .presented(keyPath($0)) },
301302
extract: {
302303
guard case let .presented(action) = $0 else { return nil }
@@ -307,10 +308,11 @@ extension PresentationAction: CasePathable {
307308

308309
@_disfavoredOverload
309310
public subscript<AppendedAction>(
310-
dynamicMember keyPath: _SendableCaseKeyPath<Action, AppendedAction>
311+
dynamicMember keyPath: CaseKeyPath<Action, AppendedAction>
311312
) -> AnyCasePath<PresentationAction, PresentationAction<AppendedAction>>
312313
where Action: CasePathable {
313-
AnyCasePath<PresentationAction, PresentationAction<AppendedAction>>(
314+
let keyPath = keyPath.unsafeSendable()
315+
return AnyCasePath<PresentationAction, PresentationAction<AppendedAction>>(
314316
embed: {
315317
switch $0 {
316318
case .dismiss:

Sources/ComposableArchitecture/SharedState/Shared.swift

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,9 @@ public struct Shared<Value: Sendable>: Sendable {
6464
reference: base.reference,
6565
// NB: Can get rid of bitcast when this is fixed:
6666
// https://github.com/swiftlang/swift/issues/75531
67-
keyPath: sendableKeyPath(
68-
(base.keyPath as AnyKeyPath)
69-
.appending(path: \Value?.[default: DefaultSubscript(initialValue)])!
70-
)
67+
keyPath: (base.keyPath as AnyKeyPath)
68+
.appending(path: \Value?.[default: DefaultSubscript(initialValue)])!
69+
.unsafeSendable()
7170
)
7271
}
7372

@@ -179,9 +178,9 @@ public struct Shared<Value: Sendable>: Sendable {
179178
reference: self.reference,
180179
// NB: Can get rid of bitcast when this is fixed:
181180
// https://github.com/swiftlang/swift/issues/75531
182-
keyPath: sendableKeyPath(
183-
(self.keyPath as AnyKeyPath).appending(path: keyPath)!
184-
)
181+
keyPath: (self.keyPath as AnyKeyPath)
182+
.appending(path: keyPath)!
183+
.unsafeSendable()
185184
)
186185
}
187186

@@ -459,11 +458,7 @@ extension Shared {
459458
) -> SharedReader<Member> {
460459
SharedReader<Member>(
461460
reference: self.reference,
462-
// NB: Can get rid of bitcast when this is fixed:
463-
// https://github.com/swiftlang/swift/issues/75531
464-
keyPath: sendableKeyPath(
465-
(self.keyPath as AnyKeyPath).appending(path: keyPath)!
466-
)
461+
keyPath: (self.keyPath as AnyKeyPath).appending(path: keyPath)!.unsafeSendable()
467462
)
468463
}
469464

Sources/ComposableArchitecture/SwiftUI/Binding.swift

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -174,18 +174,20 @@ public struct BindingAction<Root>: CasePathable, Equatable, Sendable {
174174
@dynamicMemberLookup
175175
public struct AllCasePaths {
176176
public subscript<Value: Equatable & Sendable>(
177-
dynamicMember keyPath: _SendableWritableKeyPath<Root, Value>
177+
dynamicMember keyPath: WritableKeyPath<Root, Value>
178178
) -> AnyCasePath<BindingAction, Value> where Root: ObservableState {
179-
AnyCasePath(
179+
let keyPath = keyPath.unsafeSendable()
180+
return AnyCasePath(
180181
embed: { .set(keyPath, $0) },
181182
extract: { $0.keyPath == keyPath ? $0.value as? Value : nil }
182183
)
183184
}
184185

185186
public subscript<Value: Equatable & Sendable>(
186-
dynamicMember keyPath: _SendableWritableKeyPath<Root, BindingState<Value>>
187+
dynamicMember keyPath: WritableKeyPath<Root, BindingState<Value>>
187188
) -> AnyCasePath<BindingAction, Value> {
188-
AnyCasePath(
189+
let keyPath = keyPath.unsafeSendable()
190+
return AnyCasePath(
189191
embed: { .set(keyPath, $0) },
190192
extract: { $0.keyPath == keyPath ? $0.value as? Value : nil }
191193
)
@@ -299,9 +301,10 @@ extension BindableAction {
299301

300302
extension ViewStore where ViewAction: BindableAction, ViewAction.State == ViewState {
301303
public subscript<Value: Equatable & Sendable>(
302-
dynamicMember keyPath: _SendableWritableKeyPath<ViewState, BindingState<Value>>
304+
dynamicMember keyPath: WritableKeyPath<ViewState, BindingState<Value>>
303305
) -> Binding<Value> {
304-
self.binding(
306+
let keyPath = keyPath.unsafeSendable()
307+
return self.binding(
305308
get: { $0[keyPath: keyPath].wrappedValue },
306309
send: { value in
307310
#if DEBUG
@@ -454,9 +457,10 @@ public struct BindingViewStore<State> {
454457
}
455458

456459
public subscript<Value: Equatable & Sendable>(
457-
dynamicMember keyPath: _SendableWritableKeyPath<State, BindingState<Value>>
460+
dynamicMember keyPath: WritableKeyPath<State, BindingState<Value>>
458461
) -> BindingViewState<Value> {
459-
BindingViewState(
462+
let keyPath = keyPath.unsafeSendable()
463+
return BindingViewState(
460464
binding: ViewStore(self.store, observe: { $0[keyPath: keyPath].wrappedValue })
461465
.binding(
462466
send: { value in

0 commit comments

Comments
 (0)