Type checking inconsistency with generic metatypes

generics
bug
protocols
type-casting
(Gor Gyolchanyan) #1

Greetings, Swift community!

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.

Improving the UI of generics
(Erik Little) #2

@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?

(Gor Gyolchanyan) #3

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?

(Erik Little) #4

I tried using

@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.

(Gor Gyolchanyan) #5

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.

(Jordan Rose) #6

You can see what's happening this way:

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:

func objc_conformsToProtocol(_ cls: AnyClass, _ proto: AnyProtocol)

(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.

(Mox) #7

This is perfect example why named existential types and any are needed as discussed in Improving the UI of generics

Meaning semantically more exact thing would be to say:

print(enumInstance is (any Protocol)) // true, as expected

Instance is equal to an existential that conforms to the protocol, it’s not equal to the protocol (type) itself.

Unfortunately current swift syntax obfuscates this :(