Skip to content

Commit

Permalink
Avoid sendable key paths in dynamic member lookup (#3463)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
stephencelis authored Oct 23, 2024
1 parent 59fdf28 commit 3879d2c
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 37 deletions.
74 changes: 65 additions & 9 deletions Sources/ComposableArchitecture/Internal/KeyPath+Sendable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,69 @@
public typealias _SendableCaseKeyPath<Root, Value> = CaseKeyPath<Root, Value>
#endif

@_transparent
func sendableKeyPath(
_ keyPath: AnyKeyPath
) -> _SendableAnyKeyPath {
#if compiler(>=6)
unsafeBitCast(keyPath, to: _SendableAnyKeyPath.self)
#else
keyPath
#endif
// NB: Dynamic member lookup does not currently support sendable key paths and even breaks
// autocomplete.
//
// * https://github.com/swiftlang/swift/issues/77035
// * https://github.com/swiftlang/swift/issues/77105
extension _AppendKeyPath {
@_transparent
func unsafeSendable() -> _SendableAnyKeyPath
where Self == AnyKeyPath {
#if compiler(>=6)
unsafeBitCast(self, to: _SendableAnyKeyPath.self)
#else
self
#endif
}

@_transparent
func unsafeSendable<Root>() -> _SendablePartialKeyPath<Root>
where Self == PartialKeyPath<Root> {
#if compiler(>=6)
unsafeBitCast(self, to: _SendablePartialKeyPath<Root>.self)
#else
self
#endif
}

@_transparent
func unsafeSendable<Root, Value>() -> _SendableKeyPath<Root, Value>
where Self == KeyPath<Root, Value> {
#if compiler(>=6)
unsafeBitCast(self, to: _SendableKeyPath<Root, Value>.self)
#else
self
#endif
}

@_transparent
func unsafeSendable<Root, Value>() -> _SendableWritableKeyPath<Root, Value>
where Self == WritableKeyPath<Root, Value> {
#if compiler(>=6)
unsafeBitCast(self, to: _SendableWritableKeyPath<Root, Value>.self)
#else
self
#endif
}

@_transparent
func unsafeSendable<Root, Value>() -> _SendableReferenceWritableKeyPath<Root, Value>
where Self == ReferenceWritableKeyPath<Root, Value> {
#if compiler(>=6)
unsafeBitCast(self, to: _SendableReferenceWritableKeyPath<Root, Value>.self)
#else
self
#endif
}

@_transparent
func unsafeSendable<Root, Value>() -> _SendableCaseKeyPath<Root, Value>
where Self == CaseKeyPath<Root, Value> {
#if compiler(>=6)
unsafeBitCast(self, to: _SendableCaseKeyPath<Root, Value>.self)
#else
self
#endif
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,12 @@ extension BindableAction where State: ObservableState {

extension Store where State: ObservableState, Action: BindableAction, Action.State == State {
public subscript<Value: Equatable & Sendable>(
dynamicMember keyPath: _SendableWritableKeyPath<State, Value>
dynamicMember keyPath: WritableKeyPath<State, Value>
) -> Value {
get { self.state[keyPath: keyPath] }
set {
BindingLocal.$isActive.withValue(true) {
self.send(.set(keyPath, newValue, isInvalidated: _isInvalidated))
self.send(.set(keyPath.unsafeSendable(), newValue, isInvalidated: _isInvalidated))
}
}
}
Expand Down Expand Up @@ -195,12 +195,12 @@ where
Action.ViewAction.State == State
{
public subscript<Value: Equatable & Sendable>(
dynamicMember keyPath: _SendableWritableKeyPath<State, Value>
dynamicMember keyPath: WritableKeyPath<State, Value>
) -> Value {
get { self.state[keyPath: keyPath] }
set {
BindingLocal.$isActive.withValue(true) {
self.send(.view(.set(keyPath, newValue, isInvalidated: _isInvalidated)))
self.send(.view(.set(keyPath.unsafeSendable(), newValue, isInvalidated: _isInvalidated)))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,10 +293,11 @@ extension PresentationAction: CasePathable {
}

public subscript<AppendedAction>(
dynamicMember keyPath: _SendableCaseKeyPath<Action, AppendedAction>
dynamicMember keyPath: CaseKeyPath<Action, AppendedAction>
) -> AnyCasePath<PresentationAction, AppendedAction>
where Action: CasePathable {
AnyCasePath<PresentationAction, AppendedAction>(
let keyPath = keyPath.unsafeSendable()
return AnyCasePath<PresentationAction, AppendedAction>(
embed: { .presented(keyPath($0)) },
extract: {
guard case let .presented(action) = $0 else { return nil }
Expand All @@ -307,10 +308,11 @@ extension PresentationAction: CasePathable {

@_disfavoredOverload
public subscript<AppendedAction>(
dynamicMember keyPath: _SendableCaseKeyPath<Action, AppendedAction>
dynamicMember keyPath: CaseKeyPath<Action, AppendedAction>
) -> AnyCasePath<PresentationAction, PresentationAction<AppendedAction>>
where Action: CasePathable {
AnyCasePath<PresentationAction, PresentationAction<AppendedAction>>(
let keyPath = keyPath.unsafeSendable()
return AnyCasePath<PresentationAction, PresentationAction<AppendedAction>>(
embed: {
switch $0 {
case .dismiss:
Expand Down
19 changes: 7 additions & 12 deletions Sources/ComposableArchitecture/SharedState/Shared.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,9 @@ public struct Shared<Value: Sendable>: Sendable {
reference: base.reference,
// NB: Can get rid of bitcast when this is fixed:
// https://github.com/swiftlang/swift/issues/75531
keyPath: sendableKeyPath(
(base.keyPath as AnyKeyPath)
.appending(path: \Value?.[default: DefaultSubscript(initialValue)])!
)
keyPath: (base.keyPath as AnyKeyPath)
.appending(path: \Value?.[default: DefaultSubscript(initialValue)])!
.unsafeSendable()
)
}

Expand Down Expand Up @@ -179,9 +178,9 @@ public struct Shared<Value: Sendable>: Sendable {
reference: self.reference,
// NB: Can get rid of bitcast when this is fixed:
// https://github.com/swiftlang/swift/issues/75531
keyPath: sendableKeyPath(
(self.keyPath as AnyKeyPath).appending(path: keyPath)!
)
keyPath: (self.keyPath as AnyKeyPath)
.appending(path: keyPath)!
.unsafeSendable()
)
}

Expand Down Expand Up @@ -459,11 +458,7 @@ extension Shared {
) -> SharedReader<Member> {
SharedReader<Member>(
reference: self.reference,
// NB: Can get rid of bitcast when this is fixed:
// https://github.com/swiftlang/swift/issues/75531
keyPath: sendableKeyPath(
(self.keyPath as AnyKeyPath).appending(path: keyPath)!
)
keyPath: (self.keyPath as AnyKeyPath).appending(path: keyPath)!.unsafeSendable()
)
}

Expand Down
20 changes: 12 additions & 8 deletions Sources/ComposableArchitecture/SwiftUI/Binding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,18 +174,20 @@ public struct BindingAction<Root>: CasePathable, Equatable, Sendable {
@dynamicMemberLookup
public struct AllCasePaths {
public subscript<Value: Equatable & Sendable>(
dynamicMember keyPath: _SendableWritableKeyPath<Root, Value>
dynamicMember keyPath: WritableKeyPath<Root, Value>
) -> AnyCasePath<BindingAction, Value> where Root: ObservableState {
AnyCasePath(
let keyPath = keyPath.unsafeSendable()
return AnyCasePath(
embed: { .set(keyPath, $0) },
extract: { $0.keyPath == keyPath ? $0.value as? Value : nil }
)
}

public subscript<Value: Equatable & Sendable>(
dynamicMember keyPath: _SendableWritableKeyPath<Root, BindingState<Value>>
dynamicMember keyPath: WritableKeyPath<Root, BindingState<Value>>
) -> AnyCasePath<BindingAction, Value> {
AnyCasePath(
let keyPath = keyPath.unsafeSendable()
return AnyCasePath(
embed: { .set(keyPath, $0) },
extract: { $0.keyPath == keyPath ? $0.value as? Value : nil }
)
Expand Down Expand Up @@ -299,9 +301,10 @@ extension BindableAction {

extension ViewStore where ViewAction: BindableAction, ViewAction.State == ViewState {
public subscript<Value: Equatable & Sendable>(
dynamicMember keyPath: _SendableWritableKeyPath<ViewState, BindingState<Value>>
dynamicMember keyPath: WritableKeyPath<ViewState, BindingState<Value>>
) -> Binding<Value> {
self.binding(
let keyPath = keyPath.unsafeSendable()
return self.binding(
get: { $0[keyPath: keyPath].wrappedValue },
send: { value in
#if DEBUG
Expand Down Expand Up @@ -454,9 +457,10 @@ public struct BindingViewStore<State> {
}

public subscript<Value: Equatable & Sendable>(
dynamicMember keyPath: _SendableWritableKeyPath<State, BindingState<Value>>
dynamicMember keyPath: WritableKeyPath<State, BindingState<Value>>
) -> BindingViewState<Value> {
BindingViewState(
let keyPath = keyPath.unsafeSendable()
return BindingViewState(
binding: ViewStore(self.store, observe: { $0[keyPath: keyPath].wrappedValue })
.binding(
send: { value in
Expand Down

0 comments on commit 3879d2c

Please sign in to comment.