I was surprised that the key-paths subscript setter does access the properties getter. This made this example crash:
struct DelayedMutable<Value> {
private var _value: Value? = nil
var isInitialized: Bool {
return _value != nil
}
var value: Value {
get {
guard let value = _value else {
fatalError("property accessed before being initialized")
}
return value
}
set {
_value = newValue
}
}
/// "Reset" the delegate so it can be initialized again.
mutating func reset() {
_value = nil
}
}
private var _value = DelayedMutable<Int>()
var value: Int {
get { return _value[keyPath: \.value] }
set { _value[keyPath: \.value] = newValue }
}
value = 42
What am I missing or why does the setter needs to access the getter of the destination property before setting it?
That was my first thought as well but then I realized the same. The KeyPath.swift file in the stdlib does not provide any implementation for the subscripts so I don't know where to look up the behavior.
Mutating through a keypath undergoes a full formal access of the property, since key path application does not know whether the projected value is going to be completely replaced, as if by a set, or further projected, passed inout, or partially updated. To do a formal access of a computed property, the compiler has to get the initial value into a temporary, apply the mutation to the temporary, and set the updated value. Directly set-ing a property is an optimization the compiler can do when it knows a property is computed and is being directly assigned to, but the opaque interface for a property uses the read/modify coroutines to do formal accesses, so if there is any abstraction, such as if the property is in a resilient module, is in a protocol, or is an overridable class member, or is accessed through a key path, the set optimization is not available.
We should provide some documentation about this behavior. So I guess even when the computed property from the DelayedMutable had read/modify accessors it still would crash in this case?
Actually this is an argument against using key-path for property forwarding I discussed in the property delegate thread. Thank you Joe for explaining that to me.
I would like to gain some experience with runtime/keypath so I am thinking of picking up this bug. I am not sure, but do we need to tweak getKeyPathProjectionCoroutine in SILGen to use those entry points (depending on KeyPathTypeKind)? cc @Joe_Groff
EDIT: Completely overlooked the word "coroutine" in the method name... I guess that's not the right place for setAt* methods.
I can't find where swift_setAtWritableKeyPath or swift_setAtReferenceWritableKeyPath is actually called from
I'm dying to see a fix for this bug. It simply prevents me from building nice convenience API's and a closure workaround adds way too much unnecessary boilerplate.