8

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?

Anton Belousov
  • 1,140
  • 15
  • 34

2 Answers2

14

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.

NobodyNada
  • 7,529
  • 6
  • 44
  • 51
1

It is because of convenience init(), Convenience initializers are secondary, supporting initializers for a class. You can define a convenience initializer to call a designated initializer from the same class as the convenience initializer with some of the designated initializer’s parameters set to default values.

Since, in convenience init() the super class property can't be accessed, you are getting Fatal error: could not demangle keypath type.

If you do the same in the designated init() it will work as it is sure that super class is initialized. Below code will print 0.5 as expected:

  class View: UIView {
        init() {
            super.init(frame: .zero)
            self[keyPath: \.alpha] = 0.5
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    }

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

enter image description here

Nandish
  • 1,136
  • 9
  • 16
  • This is not correct; a convenience init can call `init(frame:)` because that initializer is automatically inherited from `UIView`. Either way, there's no reason why the code should compile fine and fail at runtime with a mysterious error message about " ����XD", nor is there a reason for the behavior to be different if the key path access is moved to a function. This appears to be [a compiler bug](https://bugs.swift.org/browse/SR-12428) caused by an interaction between key paths and the dynamic `Self` type. I'm looking into it and will post a more detailed answer soon. – NobodyNada Jul 17 '20 at 23:19
  • I didn't mentioned that init(frame:) cannot be called inside a convenience init(). convenience init() must delegate across and can't delegate up the chain. the super.init() can be called only in designated init() and not the convenience init() of the child class. – Nandish Jul 18 '20 at 09:19
  • The convenience initializer is indeed delegating across in this case: it’s calling `self.init(frame:)`, not `super.init(frame:)`. `self.init(frame:)` is a designated initializer on `V` which was [automatically inherited](https://docs.swift.org/swift-book/LanguageGuide/Initialization.html#ID222) from `UIView`: “ If your subclass doesn’t define any designated initializers, it automatically inherits all of its superclass designated initializers.” – NobodyNada Jul 18 '20 at 17:03
  • 1
    Again, note that the code produces a runtime error rather than a compile-time error. If the code was misusing initializers, it would fail to compile rather than crashing at runtime. – NobodyNada Jul 18 '20 at 17:03