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?
1 Like
To set oldValue properly? Seems a little weird since didSet hasn't been defined, but it's the only thing I can think of.
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.
cc @Joe_Groff
Joe_Groff
(Joe Groff)
4
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.
3 Likes
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?
Joe_Groff
(Joe Groff)
6
There would be no way to produce an initial value for the projected value, so it seems like fundamentally you'd have to.
1 Like
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 actually wouldn't expect this to need to call the getter. We're probably insufficiently special-casing key-path application l-values.
3 Likes
So should I file a bug or not? 
Joe_Groff
(Joe Groff)
10
Is there an entry point for doing a set through a key path? I thought we only had read and modify entry points.
Yeah, there are swift_setAtWritableKeyPath and swift_setAtReferenceWritableKeyPath entrypoints.
4 Likes
Joe_Groff
(Joe Groff)
12
OK, in that case it should be implementable to do a pure set, and it is indeed a bug that we aren't using it.
4 Likes
Bumped into this bug again which disallows me to implement certain API. 
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 
1 Like
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.
I think they are not called at all, which is the bug here. I could be wrong though. 
1 Like
If we're using modifyKeyPath for simple assignments into a KeyPath, that's very wrong and should be easy to fix in SILGen.
1 Like
Running into this bug myself. Has there been any traction?
Something else odd about this bug: it doesn't happen in release builds