There's some confusing terminology in this area that is making the current implementation less obvious than it should be. I'll try to summarize quickly here to answer your question, but the ABI documentation on type layout describes how type metadata and witness tables are layed out in memory, and the generic signatures ABI document describes how generic arguments are encoded. All of this was also covered in an LLVM Developer Meeting talk in 2017 on Implementing Swift Generics. It's quite worthwhile if you'd like to understand the implementation model.
Anyway, I'll try here. There are three concepts getting munged together:
- Type metadata: a unique description of a type, such as
X
orSet<X with a specific X:Hashable conformance>
. You can get the type metadata in Swift itself: it's a value of typeAny.Type
. - Witness table: a description of a conformance of a given type (say,
X
) to a given protocol (Hashable
). These aren't unique, due to retroactive conformances. For generic types (e.g.,Set<Element>
), there is a witness table "pattern" that will be instantiated once the generic arguments are known, soSet<X with a specific X:Hashable>
-conforms-to-Hashable
can be different fromSet<Int with a specific Int:Hashable>
-conforms-to-Hashable
. - Value witness table: a special table that's like a witness table, but for the core operations on a type like copy, destroy, size/alignment, move, etc. You can't really get at this in Swift and it doesn't really matter for this discussion.
In the implementation, there is no "type witness table" that has all of the conformances. That's something @dabrahams is proposing.
To your question about how scope #1 and scope #2 can have the same type (let's use X
again) but with different conformances (e.g., to Hashable
), it's because anything generic that has a Hashable
requirement takes an implicit parameter for the witness table that implements that conformance. So something like this:
func f<T: Hashable>(_ value: T) {
print(value.hashValue)
}
takes an implicit witness table parameter for that T: Hashable
conformance. In scope #1 (e.g., module B from my original example), the caller to f
will pass along the witness table it knows about, which is defined in module B. f
will call hashValue
via the witness table itself, so it gets the version from module B.
Same deal with scope #2 (module C): it passes down a different witness table from module C, so you get different behavior.
Doug