iosswiftuikitswift5swift-keypath

Fatal error: could not demangle keypath type


I have a simple class and I want to use keypath in the init, something like this:

class V: UIView {
    convenience init() {
        self.init(frame: .zero)
        self[keyPath: \.alpha] = 0.5
    }
}

let v = View()

When I run this code I get a runtime error:

Fatal error: could not demangle keypath type from ' ����XD':

But, if I specify the type in keyPath it works fine:

class V: UIView {
    convenience init() {
        self.init(frame: .zero)
        self[keyPath: \UIView.alpha] = 0.5
    }
}

let v = View()
print(v.alpha) \\ prints 0.5

But, what's even stranger is that this code works:

class V: UIView {
    convenience init() {
        self.init(frame: .zero)
        foo()
    }
    
    func foo() { 
        self[keyPath: \.alpha] = 0.5
    }
}

let v = View()
print(v.alpha) \\ prints 0.5

What is the actual reason for this error?


Solution

  • Unsurprisingly, this is a compiler bug. In fact, it was reported only a couple weeks before you posted your question. The bug report contains a slightly simpler example that triggers the same crash:

    class Foo: NSObject {
      @objc let value: String = "test"
      
      func test() {
        let k1 = \Foo.value  // Ok
        let k2 = \Self.value // Fatal error: could not demangle keypath type from '�: file /AppleInternal/BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1103.8.25.8/swift/stdlib/public/core/KeyPath.swift, line 2623
      }
    }
    
    Foo().test()
    

    It turns out the Swift compiler was not properly handling key paths containing the covariant Self type. In case you need a refresher, the covariant Self type or dynamic Self type allows you to specify that a method or property always returns the type of self even if the class is subclassed. For example:

    class Foo {
        var invariant: Foo { return self }
        var covariant: Self { return self }
    }
    class Bar: Foo {}
    
    let a = Bar().invariant // a has compile-time type Foo
    let b = Bar().covariant // b has compile-time type Bar
    
    func walkInto(bar: Bar) {}
    walkInto(bar: a)        // error: cannot convert value of type 'Foo' to expected argument type 'Bar'
    walkInto(bar: b)        // works
    

    But your example doesn't use dynamic Self! Well actually it does: in the context of a convenience initializer, the type of self is actually the dynamic Self type, because a convenience initializer can also be called to initialize a subclass of your class V.

    So what exactly went wrong? Well, the Swift compiler did not include any logic to handle dynamic Self when creating a key path. Under-the-hood, it essentially tried to emit a key path object of type ReferenceWritableKeyPath<Self, CGFloat>. The type system doesn't allow you to use dynamic Self in that context, and the runtime was not expecting it. The strange error message you received was the result of trying to decode this unexpected object type, which was encoded as a 4-byte relative pointer to the metadata for your V class, followed by the suffix XD indicating a dynamic Self type (hence the error message containing 4 's followed by XD). By playing around with different ways to create key paths involving dynamic Self, I came across a number of different crashes at both compile-time and runtime.

    I have submitted a fix for this bug. It turned out to be pretty simple: essentially, everywhere we find a dynamic Self when creating a key path, we just replace it with the static self and add downcasts when necessary. Dynamic Self only matters to enforce program correctness at compile time; it can and should be stripped out of the program before runtime.