Keypaths subscript setter accesses the properties getter?

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

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?

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? :smiley:

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

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

Filed: [SR-10203] Subscripts setter on key-path calls getter of the destination property · Issue #52603 · apple/swift · GitHub

2 Likes

Bumped into this bug again which disallows me to implement certain API. :face_with_head_bandage:

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 :thinking:

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. :smiley:

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