Hi all,

The main missing piece for conditional conformances 
(https://github.com/apple/swift-evolution/blob/master/proposals/0143-conditional-conformances.md)
 is support for dynamic casting. Right now, dynamic casting that queries a 
conditional conformance will always fail. Here’s an example:

protocol P {
  func foo()
}

struct X : P {
  func foo() { print("X.P") }
}

struct Y<T> {
  var wrapped: T
}

extension Y: P where T: P {
  func foo() { wrapped.foo() }
}

func tryAsP(_ value: Any) {
  if let p = value as? P {
    p.foo()
  }
}

let yx = Y(wrapped: X())
tryAsP(yx)

This won’t print anything, but should print “X.P”. We’d like to fix that :)

Joe Groff, Slava, and I discussed an approach to implement dynamic casting for 
conditional conformances, which I’d like to outline here.

Background
The Swift runtime expresses the conformance of a specific type (say Array<Int>) 
to a particular protocol (say, Equatable) using a witness table. Witness tables 
contain the actual associated types as well as function pointers for each of 
the requirements of a protocol. Slava and John’s talk on Implementing Swift 
Generics (https://llvm.org/devmtg/2017-10/#talk15) gives a bunch of background 
here.

When a conformance is conditional, the witness table also stores the 
conditional requirements. So, the witness table for Array<Int>: Equatable has 
to store the witness table Int: Equatable. In the example above, the witness 
table for Y<X>: P needs to store a pointer to the witness table X: P. The 
compiler knows how to pass along the witness tables needed for a conditional 
conformance, but for runtime casting to work, we need to (1) evaluate all of 
the conditional requirements to determine whether they apply (e.g., does T: P 
for a specific T?) and then (2) pass those witness tables along when building 
the witness table. The cast should fail if any of the conditional requirements 
goes unsatisfied, and produce a valid witness table otherwise.

Protocol Conformance Records
Whenever code declares conformance of a particular type to a given protocol, 
the compiler emits a “protocol conformance record” (documented at 
https://github.com/apple/swift/blob/master/docs/ABI/TypeMetadata.rst#protocol-conformance-records).
 These protocol conformance records state the type declaring conformance (e.g., 
Y) and the protocol (e.g., P), and then provide a witness table accessor to 
form the witness table given type metadata for a specific type instance (e.g., 
Y<X>). In the case of a conditional conformance, this accessor also needs to be 
passed the witness tables for any conditional requirements, e.g., the witness 
table for X: P. 

Conditional Requirements in Protocol Conformance Records
Somehow we need to dynamically evaluate the conditional requirements. For 
example, we need to know that for the X<T>: P conformance to be valid, we need 
T: P to hold for the given T. So, we’ll extend the protocol conformance record 
with an encoding of those requirements. Fortunately, we already have a way to 
encode arbitrary generic requirements: the mangling of a generic-signature (see 
https://github.com/apple/swift/blob/master/docs/ABI/Mangling.rst#generics) 
already encodes arbitrary generic requirements, so we can put a string 
comprised of the mangled conditional requirements into the protocol conformance 
record.

When evaluating a conditional conformance, we demangle the conditional 
requirements to get something like: “T must conform to P”. We then need to 
substitute the type arguments (e.g., X) for the  corresponding type parameters 
(T) to form type metadata for the requirements. In this case, we’d get the type 
metadata pointer for Y and the protocol descriptor for P, and then call 
swift_conformsToProtocol() to determine whether the requirement holds. If it 
does, swift_conformsToProtocol() produces the Y: P witness table we need to 
pass along to the witness table accessor.

Note that the conditional requirements can be arbitrarily complicated. For 
example, given:

extension Dictionary: P where Value == (Key) -> Bool { … }

Even though the result of evaluating this same-type requirement doesn’t need to 
be passed along to the witness table accessor, we still need to evaluate the 
requirement to determine whether the conditional conformance to P applies. To 
do so, we will need to substitute type arguments for both Value and Key, and 
need to form the type metadata representing the (substituted) function type 
(Key) -> Bool to determine if it is equivalent to the (substituted) type of 
Value (which is then determined by type metadata pointer equality because the 
runtime uniques type metadata). 

Looking up Type Metadata For a Mangled Name
To perform that mapping from a mangled type in a conditional requirement to 
type metadata, we effectively need an operation to take a mangled type name and 
turn it into a type metadata pointer. This is something we could surface in the 
Swift standard library/runtime as, e.g.,

func type(named: String) -> Any.Type?

to take a mangled type name and try to get the type metadata for it. From 
there, one can query protocol conformances that (say) allow one to construct 
instances of the arbitrarily-named type. Think of it as NSClassFromString for 
any type in the Swift language, including specializations of generic types.

To get here, we’ll also need to extend the nominal type descriptor 
(https://github.com/apple/swift/blob/master/docs/ABI/TypeMetadata.rst#nominal-type-descriptor)
 to contain the generic signature of a nominal type. That let’s us safely 
create specializations of generic types. For example, if one tries to form 
Dictionary<A, B> where A does not conform to Hashable, type(named:) should 
return “nil” rather than an invalid type. The same logic that will be used to 
form type metadata when checking conditional requirements will apply here. 
Indeed, it’s probably worth proposing type(named:) as a separate language 
feature on the path to conditional conformances.

Looking Forward: Generalized Existentials
Generalized existentials 
(https://github.com/apple/swift/blob/master/docs/GenericsManifesto.md#generalized-existentials)
 allows us to express more kinds of “existential” type in the language, e.g., 
“A Collection of some type of Element that we know is Equatable”, e.g.,

var a: Any<Collection where .Element: Equatable>
a = [1, 2, 3, 4, 5]
a = Set([“a”, “b”, “c”])

To get there, we can change (or extend) the protocol metadata 
(https://github.com/apple/swift/blob/master/docs/ABI/TypeMetadata.rst#protocol-metadata)
 to contain a complete generic signature, which can be evaluated dynamically 
using the same mechanisms described above. For example:

func f(any: Any) {
  if let c = any as? Any<Collection where .Element: Equatable> { … }
}

We should make the metadata change to allow this form of generalized 
existentials before locking down the ABI, even if we don’t settle on the 
feature in the surface language.

Thoughts?

        - Doug

_______________________________________________
swift-dev mailing list
swift-dev@swift.org
https://lists.swift.org/mailman/listinfo/swift-dev

Reply via email to