KeyPath created within an extension has an unexpected hash value


(Adrian Zubarev) #1

I'm trying to workaround missing identity key-path in Swift 4.2 with a custom protocol that adds a _self property. However when I create a partial key-path from within an extension of my other protocol it produces a key-path with an unexpected hash value which would always fail the equality test later on.

Am I doing something wrong or is this a weird bug?

prefix operator ^

prefix func ^ <Root, Value>(keyPath: KeyPath<Root, Value>) -> (Root) -> Value {
  return { root in root[keyPath: keyPath] }
}

prefix func ^ <Root, Value>(
  keyPath: KeyPath<Root, Value>
) -> PartialKeyPath<Root> {
  return keyPath
}

protocol _Identity {
  var _self: Self { get }
}

extension _Identity {
  var _self: Self {
    return self
  }
}

protocol Configurator {
  associatedtype Driver: _Identity
  typealias PartialKeyPathSet = Set<PartialKeyPath<Driver>>
  func configure(_ driver: inout Driver, at keyPaths: PartialKeyPathSet)
}

extension Configurator {
  func configure(_ driver: inout Driver) {
    #warning("FIXME: Remove the underscore in Swift 5.")
    // WHY DOES THIS GENERATE SOMETHING UNEXPECTED?
    configure(&driver, at: [^\._self]) 
  }
}

extension Collection where Element: Equatable {
  func containsAny<C>(
    from other: C
  ) -> Bool where C: Collection, C.Element == Element {
    return other.contains(where: self.contains)
  }
}

struct S: _Identity, Hashable {
  var isAvailable = false
  var value = 42
}

struct Tester: Configurator {
  typealias Driver = S
  func configure(_ driver: inout Driver, at keyPaths: PartialKeyPathSet) {
    let identifyKeyPath = \Driver._self
    if keyPaths.containsAny(from: [^\._self, ^\.isAvailable]) {
      driver.isAvailable = Bool.random()
      print("works")
    }
    if keyPaths.contains(^\.value) {
      driver.value = Int.random(in: 0 ... 10)
    }

    print("""
      identify key path: \(identifyKeyPath)
      identity key path hash value: \(identifyKeyPath.hashValue)
      keyPaths: \(keyPaths)
      keyPaths (hash values): \(keyPaths.map(^\.hashValue))
      keyPaths contains identity key path: \(keyPaths.contains(identifyKeyPath))

      """)
  }
}

let tester = Tester()
var s = S()
tester.configure(&s, at: [^\._self])
tester.configure(&s) // This line will use the method from the extension.
tester.configure(&s, at: [^\._self])

The output of this looks always like this (I don't mean that the hash values remain during each run though):

works
identify key path: Swift.KeyPath<__lldb_expr_11.S, __lldb_expr_11.S>
identity key path hash value: -6714669026563237229
keyPaths: [Swift.KeyPath<__lldb_expr_11.S, __lldb_expr_11.S>]
keyPaths (hash values): [-6714669026563237229]
keyPaths contains identity key path: true

identify key path: Swift.KeyPath<__lldb_expr_11.S, __lldb_expr_11.S>
identity key path hash value: -6714669026563237229
keyPaths: [Swift.KeyPath<__lldb_expr_11.S, __lldb_expr_11.S>]
keyPaths (hash values): [1953250547225779771] 😱
keyPaths contains identity key path: false

works
identify key path: Swift.KeyPath<__lldb_expr_11.S, __lldb_expr_11.S>
identity key path hash value: -6714669026563237229
keyPaths: [Swift.KeyPath<__lldb_expr_11.S, __lldb_expr_11.S>]
keyPaths (hash values): [-6714669026563237229]
keyPaths contains identity key path: true

cc @Joe_Groff


(Adrian Zubarev) #2

I reduced the example to this, it seems it applies to any key-path from the extension:

prefix operator ^

prefix func ^ <Root, Value>(keyPath: KeyPath<Root, Value>) -> (Root) -> Value {
  return { root in root[keyPath: keyPath] }
}

prefix func ^ <Root, Value>(
  keyPath: KeyPath<Root, Value>
) -> PartialKeyPath<Root> {
  return keyPath
}

protocol P {
  var value: Int { get }
  var this: Self { get }
}

protocol Q {
  associatedtype SomeP: P
}

extension Q {
  func _print(_ keyPaths: [PartialKeyPath<SomeP>]) {
    print("""
      keyPaths: \(keyPaths)
      keyPaths (hash values): \(keyPaths.map(^\.hashValue))

      """)
  }

  func _printFromExtension() {
    _print([^\.this, ^\.value])
  }
}

struct _P: P {
  var value: Int = 42

  var this: _P {
    return self
  }
}

struct _Q: Q {
  typealias SomeP = _P
}

let q = _Q()
q._printFromExtension()
q._print([^\.this, ^\.value])
q._print([^\.this, ^\.value])
keyPaths: [Swift.KeyPath<__lldb_expr_5._P, __lldb_expr_5._P>, Swift.KeyPath<__lldb_expr_5._P, Swift.Int>]
keyPaths (hash values): [-7448712733925955122, -7256284227400383857]

keyPaths: [Swift.KeyPath<__lldb_expr_5._P, __lldb_expr_5._P>, Swift.WritableKeyPath<__lldb_expr_5._P, Swift.Int>]
keyPaths (hash values): [-7868618874185739794, 5640747722181805698]

keyPaths: [Swift.KeyPath<__lldb_expr_5._P, __lldb_expr_5._P>, Swift.WritableKeyPath<__lldb_expr_5._P, Swift.Int>]
keyPaths (hash values): [-7868618874185739794, 5640747722181805698]

(Joe Groff) #3

This is a known issue; it's an unfortunate bit of the implementation model leaking out. The protocol requirement is an independent declaration from the extension method or any concrete implementations in concrete types, so a reference to \._self on the protocol type or a generic type constrained by the protocol, which will end up referencing the requirement, is considered not equal to \._self on the concrete type, which will end up referencing the implementation declaration.


(Adrian Zubarev) #4

I read your comment twice but I'm still not sure I fully follow. Are you saying this is an issues that was overseen during the original implementation and requires someone to fix it someday, or is it an unfixable artifact of key-paths? Do you have any workaround for this, as ugly it can be I'll take any?! In my particular case I will probably drop the extension method until I can write \.self, but I really don't know if I ever build anything similar somewhere else which would already result in unexpected behavior.

Should I file a bug still? (This issue is also present in Swift 5.)


(Joe Groff) #5

As far as a workaround goes, you can ensure that \._self is always formed in a generic context, say by adding a helper method to your protocol:

extension _Identity {
  static func selfKeyPath() -> WritableKeyPath<Self, Self> { return \._self }
}

Sorry my explanation wasn't clear. It's a bit like overload resolution. If you had the following declarations:

protocol P {}
extension String: P {}

func foo<T: P>(_: T) {}
func foo(_: String) {}

then from a generic context where you have a variable x: T where T: P, calling foo(x) would always pick the first overload of foo, whereas if you were in a context where x: String, calling foo(x) would pick the second. A similar situation exists between protocol requirements and their implementations. When you look up a protocol member on a generic type, overload resolution sees only the protocol requirements followed by extension members. On a concrete type, overload resolution sees only the implementations. Normally you don't notice this, because the runtime behavior works out the same and the identity of the decl chosen doesn't semantically matter, but key paths end up exposing this by making the identity of declarations impact equality and hashing.

This could conceivably be fixed by adjusting key path member lookup to always favor protocol requirements in the compiler. There's a bug open for this; I haven't had time yet to evaluate the impact of trying this.


(Adrian Zubarev) #6

I'm not quite sure how this should help, I tried similar approaches already but all of them resulted in the same situation.

Thank you for the more in depth explanation on the issue.


(Joe Groff) #7

There may in fact be a bug in that case. I would expect the _Identity.selfKeyPath function to return an equivalent key path when invoked on the same Self type.