There is an inconsistent behavior of the type checking primitives with generic metatypes.
The following snippet of code demonstrates the problem and has been tested with Xcode 10.2 (10E125) using its builtin toolchain:
protocol Protocol { }
enum Enum: Protocol { case `case` }
struct Struct: Protocol { }
class Class: Protocol { }
let enumInstance: Any = Enum.case
let structInstance: Any = Struct()
let classInstance: Any = Class()
print(enumInstance is Protocol) // true, as expected
print(structInstance is Protocol) // true, as expected
print(classInstance is Protocol) // true, as expected
func instanceCheck<TheType>(_ instance: Any, _: TheType.Type) -> Bool {
return instance is TheType
}
print(instanceCheck(enumInstance, Protocol.self)) // true, as expected
print(instanceCheck(structInstance, Protocol.self)) // true, as expected
print(instanceCheck(classInstance, Protocol.self)) // true, as expected
let enumType: Any.Type = Enum.self
let structType: Any.Type = Struct.self
let classType: Any.Type = Class.self
print(enumType is Protocol.Type) // true, as expected
print(structType is Protocol.Type) // true, as expected
print(classType is Protocol.Type) // true, as expected
func typeCheck<TheType>(_ type: Any.Type, _: TheType.Type) -> Bool {
return type is TheType.Type
}
print(typeCheck(enumType, Protocol.self)) // false, surprisingly
print(typeCheck(structType, Protocol.self)) // false, surprisingly
print(typeCheck(classType, Protocol.self)) // false, surprisingly
Can anyone determine whether or not this behavior is intentional?
This inconsistency is a major blocker on my way toward implementing a type-safe dynamic dispatcher.
If this behavior is intentional, then might I enquire about the reasoning and if this behavior is indeed a bug, then what is the most probable cause?
P.S. This is only reproducible for protocol metatypes. The exact same code checking for class metatype relationships works as expected.
@Joe_Groff can probably explain a bit more, but IIRC, swift_dynamicCastMetatype, which is the Swift runtime function that typeCheck probably uses, doesn't check for protocol conformances. AFAIU, it checks subclassing relationships and some other things, but at the end, it falls back to comparing the metadata pointers.
As for the enumType is Protocol.Type bits, maybe this is handled at compile time and can be verified to be true, and so just gets optimized to a straight print?
I wonder if will it still be true if the enumType, structType and classType would not be statically analyzable by the compiler. Can you think of a surefire way to make them non-analyzable?
@inline(never)
var enumType: Any.Type {
return Enum.self
}
print(enumType is Protocol.Type)
but it seems the compiler is smart enough in this context to know it should call swift_conformsToProtocol. Whereas inside of typeCheck, it only knows it's working with some generic metadata. It doesn't seem forcing inlining helps any, I wonder if that could be considered an improvement worth filing a bug report for.
Hm... If this behavior is indeed due to clever static analysis, rather than a bug in the type checking system, then I think the one surefire way to test this definitively would be:
Module1
public protocol Protocol { }
private struct Struct: Protocol { }
private var structType: Any.Type = Struct.self
public let structTypePointer: UnsafeRawPointer = .init(UnsafePointer(&structType))
Module2
import Module1
let structType = Module1.structTypePointer.assumingMemoryBoundTo(Any.Type.self).pointee
print(structType is Module1.Protocol.Type)
Of course, Module1 and Module2 should be compiled separately and Module1 has to have no trace of source code and debug symbols available for Module2.
protocol MyProto {}
struct S: MyProto {}
let instance: MyProto = S()
let cls: MyProto.Type = S.self
let proto = MyProto.self
func printType<T>(_ value: T) {
print(value, T.self)
}
printType(instance) // S() MyProto
printType(cls) // S MyProto.Type
printType(proto) // MyProto MyProto.Protocol
That is, MyProto.Type and MyProto.Protocol are different types. One of them is the type of "types that conform to the protocol"; the other is the type of "the protocol itself". (In the language of Improving the UI of generics, the former is (any MyProto).Type.)
Okay, they're different, but why? (Since this is annoying.) The answer is because Objective-C has a function to dynamically check protocol conformances that looks something like this:
(Objective-C actually just calls this type "Protocol", but I've named it "AnyProtocol" to go with "AnyClass".)
The existence of this function means that protocols themselves have to have a type, and that type is distinct from "types that conform to the protocol". That's the design for any sort of reflection that acts on protocols themselves, as opposed to the types that conform to them.