[CSBindings] Downgrade key path mutability based on contextual information

If key path literal is converted to a read-only type, let's not
use maximum mutability to avoid unnecessary conversions. This is
also important because accessor references are availability checked
and we need to avoid referencing something that is not actually
going to be used.

There are some edge-cases in this approach which would still
produce a conversions, i.e.:

```
struct S {
    var a: Int
    let b: Int
}

func test<T>(_: T, _: T) {}

test(\S.a, \S.b)
```

Here `\S.a` is going to be converted to `KeyPath<S, Int>`. Availability
checker would still have to recognize situations like that and skip
checking setters if they are not used.
This commit is contained in:
Pavel Yaskevich
2025-09-24 10:27:43 -07:00
parent cf3dab10bb
commit 02696c9aba
3 changed files with 30 additions and 7 deletions

View File

@@ -721,6 +721,7 @@ bool BindingSet::finalize(bool transitive) {
if (isValid && !capability)
return false;
bool isContextualTypeReadOnly = false;
// If the key path is sufficiently resolved we can add inferred binding
// to the set.
SmallSetVector<PotentialBinding, 4> updatedBindings;
@@ -740,19 +741,30 @@ bool BindingSet::finalize(bool transitive) {
}
updatedBindings.insert(binding.withType(fnType));
isContextualTypeReadOnly = true;
} else if (!(bindingTy->isWritableKeyPath() ||
bindingTy->isReferenceWritableKeyPath())) {
isContextualTypeReadOnly = true;
}
}
// Note that even though key path literal maybe be invalid it's
// still the best course of action to use contextual function type
// bindings because they allow to propagate type information from
// the key path into the context, so key path bindings are addded
// the key path into the context, so key path bindings are added
// only if there is absolutely no other choice.
if (updatedBindings.empty()) {
auto rootTy = CS.getKeyPathRootType(keyPath);
// A valid key path literal.
if (capability) {
// Capability inference always results in a maximum mutability
// but if context is read-only it can be downgraded to avoid
// conversions.
if (isContextualTypeReadOnly)
capability =
std::make_pair(KeyPathMutability::ReadOnly, capability->second);
// Note that the binding is formed using root & value
// type variables produced during constraint generation
// because at this point root is already known (otherwise

View File

@@ -61,7 +61,7 @@ do {
let _: KeyPath<K, Bool> = \.[NonSendable()] // ok
let _: KeyPath<K, Bool> & Sendable = \.[NonSendable()] // expected-warning {{type 'KeyPath<K, Bool>' does not conform to the 'Sendable' protocol}}
let _: KeyPath<K, Int> & Sendable = \.[42, NonSendable(data: [-1, 0, 1])] // expected-warning {{type 'ReferenceWritableKeyPath<K, Int>' does not conform to the 'Sendable' protocol}}
let _: KeyPath<K, Int> & Sendable = \.[42, NonSendable(data: [-1, 0, 1])] // expected-warning {{type 'KeyPath<K, Int>' does not conform to the 'Sendable' protocol}}
let _: KeyPath<K, Int> & Sendable = \.[42, -1] // Ok
test(nonSendableKP) // expected-warning {{type 'KeyPath<K, Bool>' does not conform to the 'Sendable' protocol}}
@@ -106,7 +106,7 @@ do {
// expected-warning@-1 {{converting non-Sendable function value to '@Sendable (V) -> Int' may introduce data races}}
let _: KeyPath<V, Int> & Sendable = \.[42, CondSendable(NonSendable(data: [1, 2, 3]))]
// expected-warning@-1 {{type 'ReferenceWritableKeyPath<V, Int>' does not conform to the 'Sendable' protocol}}
// expected-warning@-1 {{type 'KeyPath<V, Int>' does not conform to the 'Sendable' protocol}}
let _: KeyPath<V, Int> & Sendable = \.[42, CondSendable(42)] // Ok
struct Root {
@@ -114,14 +114,14 @@ do {
}
testSendableKP(v: v, \.[42, CondSendable(NonSendable(data: [1, 2, 3]))])
// expected-warning@-1 {{type 'ReferenceWritableKeyPath<V, Int>' does not conform to the 'Sendable' protocol}}
// expected-warning@-1 {{type 'KeyPath<V, Int>' does not conform to the 'Sendable' protocol}}
testSendableFn(v: v, \.[42, CondSendable(NonSendable(data: [1, 2, 3]))])
// expected-warning@-1 {{converting non-Sendable function value to '@Sendable (V) -> Int' may introduce data races}}
testSendableKP(v: v, \.[42, CondSendable(42)]) // Ok
let nonSendable = NonSendable()
testSendableKP(v: v, \.[42, CondSendable(nonSendable)])
// expected-warning@-1 {{type 'ReferenceWritableKeyPath<V, Int>' does not conform to the 'Sendable' protocol}}
// expected-warning@-1 {{type 'KeyPath<V, Int>' does not conform to the 'Sendable' protocol}}
testSendableFn(v: v, \.[42, CondSendable(nonSendable)])
// expected-warning@-1 {{converting non-Sendable function value to '@Sendable (V) -> Int' may introduce data races}}
@@ -277,3 +277,14 @@ do {
// expected-warning@-1 {{type 'KeyPath<Foo, Int>' does not conform to the 'Sendable' protocol; this is an error in the Swift 6 language mode}}
}
}
public final class TestSetterRef {
public internal(set) var v: Int = 0 // expected-note {{setter for property 'v' is not '@usableFromInline' or public}}
public func test1(_ kp: KeyPath<TestSetterRef, Int> = \.v) {} // Ok
public func test2(_ kp: KeyPath<TestSetterRef, Int> = \TestSetterRef.v) {} // Ok
public func test3(_ kp: KeyPath<TestSetterRef, Int> & Sendable = \.v) {} // Ok
public func test_err(_ kp: WritableKeyPath<TestSetterRef, Int> = \.v) {}
// expected-warning@-1 {{setter for property 'v' is internal and should not be referenced from a default argument value}}
}

View File

@@ -738,11 +738,11 @@ class M {
// CHECK-LABEL: // test_metatype_keypaths()
// CHECK-LABEL: sil hidden [ossa] @{{.*}} : $@convention(thin) () -> () {
func test_metatype_keypaths() {
// CHECK: keypath $ReferenceWritableKeyPath<M.Type, Int>, (root $M.Type; settable_property $Int, id @$s8keypaths1MC10chanceRainSivgZ : $@convention(method) (@thick M.Type) -> Int, getter @$s8keypaths1MC10chanceRainSivpZACmTK : $@convention(keypath_accessor_getter) (@in_guaranteed @thick M.Type) -> @out Int, setter @$s8keypaths1MC10chanceRainSivpZACmTk : $@convention(keypath_accessor_setter) (@in_guaranteed Int, @in_guaranteed @thick M.Type) -> ())
// CHECK: keypath $KeyPath<M.Type, Int>, (root $M.Type; settable_property $Int, id @$s8keypaths1MC10chanceRainSivgZ : $@convention(method) (@thick M.Type) -> Int, getter @$s8keypaths1MC10chanceRainSivpZACmTK : $@convention(keypath_accessor_getter) (@in_guaranteed @thick M.Type) -> @out Int, setter @$s8keypaths1MC10chanceRainSivpZACmTk : $@convention(keypath_accessor_setter) (@in_guaranteed Int, @in_guaranteed @thick M.Type) -> ())
let _: KeyPath<M.Type, Int> = \M.Type.chanceRain
// CHECK: keypath $KeyPath<M.Type, Bool>, (root $M.Type; gettable_property $Bool, id @$s8keypaths1MC7isSunnySbvgZ : $@convention(method) (@thick M.Type) -> Bool, getter @$s8keypaths1MC7isSunnySbvpZACmTK : $@convention(keypath_accessor_getter) (@in_guaranteed @thick M.Type) -> @out Bool)
let _: KeyPath<M.Type, Bool> = \M.Type.isSunny
// CHECK: keypath $ReferenceWritableKeyPath<M.Type, Bool>, (root $M.Type; settable_property $Bool, id @$s8keypaths1MC8isCloudySbvgZ : $@convention(method) (@thick M.Type) -> Bool, getter @$s8keypaths1MC8isCloudySbvpZACmTK : $@convention(keypath_accessor_getter) (@in_guaranteed @thick M.Type) -> @out Bool, setter @$s8keypaths1MC8isCloudySbvpZACmTk : $@convention(keypath_accessor_setter) (@in_guaranteed Bool, @in_guaranteed @thick M.Type) -> ())
// CHECK: keypath $KeyPath<M.Type, Bool>, (root $M.Type; settable_property $Bool, id @$s8keypaths1MC8isCloudySbvgZ : $@convention(method) (@thick M.Type) -> Bool, getter @$s8keypaths1MC8isCloudySbvpZACmTK : $@convention(keypath_accessor_getter) (@in_guaranteed @thick M.Type) -> @out Bool, setter @$s8keypaths1MC8isCloudySbvpZACmTk : $@convention(keypath_accessor_setter) (@in_guaranteed Bool, @in_guaranteed @thick M.Type) -> ())
let _: KeyPath<M.Type, Bool> = \M.Type.isCloudy
// CHECK: keypath $KeyPath<M.Type, String>, (root $M.Type; gettable_property $String, id @$s8keypaths1MCySSSicigZ : $@convention(method) (Int, @thick M.Type) -> @owned String, getter @$s8keypaths1MCySSSicipZACmTK : $@convention(keypath_accessor_getter) (@in_guaranteed @thick M.Type, @in_guaranteed Int) -> @out String, indices [%$0 : $Int : $Int], indices_equals @$sSiTH : $@convention(keypath_accessor_equals) (@in_guaranteed Int, @in_guaranteed Int) -> Bool, indices_hash @$sSiTh : $@convention(keypath_accessor_hash) (@in_guaranteed Int) -> Int) (%{{.*}})
let _: KeyPath<M.Type, String> = \M.Type.[2]