Skip to content

Commit

Permalink
Avoid sendable key paths in dynamic member lookup (#245)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
stephencelis authored Oct 23, 2024
1 parent f060948 commit 07a99a9
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 8 deletions.
27 changes: 27 additions & 0 deletions Sources/SwiftNavigation/Internal/KeyPath+Sendable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,30 @@
public typealias _SendableKeyPath<Root, Value> = KeyPath<Root, Value>
public typealias _SendableWritableKeyPath<Root, Value> = WritableKeyPath<Root, Value>
#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<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
}
}
16 changes: 8 additions & 8 deletions Sources/SwiftNavigation/UIBinding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -374,11 +374,11 @@ public struct UIBinding<Value>: Sendable {
/// - Parameter keyPath: A key path to a specific resulting value.
/// - Returns: A new binding.
public subscript<Member>(
dynamicMember keyPath: _SendableWritableKeyPath<Value, Member>
dynamicMember keyPath: WritableKeyPath<Value, Member>
) -> UIBinding<Member> {
func open(_ location: some _UIBinding<Value>) -> UIBinding<Member> {
UIBinding<Member>(
location: _UIBindingAppendKeyPath(base: location, keyPath: keyPath),
location: _UIBindingAppendKeyPath(base: location, keyPath: keyPath.unsafeSendable()),
transaction: transaction
)
}
Expand All @@ -391,12 +391,12 @@ public struct UIBinding<Value>: Sendable {
/// - Returns: A new binding.
@_disfavoredOverload
public subscript<Member>(
dynamicMember keyPath: _SendableKeyPath<Value.AllCasePaths, AnyCasePath<Value, Member>>
dynamicMember keyPath: KeyPath<Value.AllCasePaths, AnyCasePath<Value, Member>>
) -> UIBinding<Member>?
where Value: CasePathable {
func open(_ location: some _UIBinding<Value>) -> UIBinding<Member?> {
UIBinding<Member?>(
location: _UIBindingEnumToOptionalCase(base: location, keyPath: keyPath),
location: _UIBindingEnumToOptionalCase(base: location, keyPath: keyPath.unsafeSendable()),
transaction: transaction
)
}
Expand All @@ -408,12 +408,12 @@ public struct UIBinding<Value>: Sendable {
/// - Parameter keyPath: A key path to a specific value.
/// - Returns: A new binding.
public subscript<Wrapped, Member>(
dynamicMember keyPath: _SendableWritableKeyPath<Wrapped, Member>
dynamicMember keyPath: WritableKeyPath<Wrapped, Member>
) -> UIBinding<Member?>
where Value == Wrapped? {
func open(_ location: some _UIBinding<Value>) -> UIBinding<Member?> {
UIBinding<Member?>(
location: _UIBindingOptionalToMember(base: location, keyPath: keyPath),
location: _UIBindingOptionalToMember(base: location, keyPath: keyPath.unsafeSendable()),
transaction: transaction
)
}
Expand All @@ -425,12 +425,12 @@ public struct UIBinding<Value>: Sendable {
/// - Parameter keyPath: A case key path to a specific associated value.
/// - Returns: A new binding.
public subscript<V: CasePathable, Member>(
dynamicMember keyPath: _SendableKeyPath<V.AllCasePaths, AnyCasePath<V, Member>>
dynamicMember keyPath: KeyPath<V.AllCasePaths, AnyCasePath<V, Member>>
) -> UIBinding<Member?>
where Value == V? {
func open(_ location: some _UIBinding<Value>) -> UIBinding<Member?> {
UIBinding<Member?>(
location: _UIBindingOptionalEnumToCase(base: location, keyPath: keyPath),
location: _UIBindingOptionalEnumToCase(base: location, keyPath: keyPath.unsafeSendable()),
transaction: transaction
)
}
Expand Down

0 comments on commit 07a99a9

Please sign in to comment.