Ordinary base protocols use fixed-indexing to access the base index. That means adding another base protocol to an existing protocol can break the order of the entries, and thus clients, because we otherwise order the base entires with TypeDecl::compare. Reparentable protocols are meant to be resilient to that, so we order them at the end of the base entries list, just before the other resilient entries in the witness table. This patch completes the picture, by having the reparentable protocol entries be indexed resiliently, in the same manner as associated conformances. The difference is that we can skip the call to `swift_getAssociatedConformanceWitness` and compute the index directly by finding the distance of the descriptors, because we know all base protocol witness table entries are eagarly instantiated. Using this distance protects us from the ordering problems of entries among all of the reparentable base protocols. resolves rdar://173409851
6.4 KiB
Reparenting Resilient Protocols
Status: This is an experimental feature available via
-enable-experimental-feature ReparentingBe very cautious when using this feature!
Overview
Suppose you have an existing protocol Contract,
// Existing protocol
public protocol Contract {
associatedtype ID
func getID() -> ID
// ...
}
and it has mixed in some notions of identity you'd like to factor out into a new protocol, Identity:
// New parent protocol (proposed)
public protocol Identity {
associatedtype ID
associatedtype Validator
var id: ID { get }
func getValidator() -> Validator
}
Since all Contract-conforming types already have an equivalent notion of "identity", it would be nice if all types already conforming to Contract now also conform to Identity. Library evolution mode rules out the ability to have Contract inherit from another protocol in a binary-compatible way, unless it is via reparenting.
To reparent Contract with the new protocol Identity, define an extension of Contract with an inheritance clause
declaring it is @reparented by Identity:
extension Contract: @reparented Identity { /* ... implementation ... */ }
This extension declares that Contract was reparented by Identity and is used to synthesize a "default conformance" for
all types conforming to Contract and were not rebuilt in the new world where Contract inherits from Identity.
The default conformance created for some Contract : Identity will be used anytime
a client built prior to the reparenting links against the library. If the client
simply recompiles against the new library, each nominal type conforming to Contract will
automatically have their own conformance to Identity created, as usual.
Thus, a default conformance serves to transition clients to a world in which the protocol inheritance relationship does exist.
Here's a complete example of reparenting Contract with Identity, with numbered points for discussion:
public protocol Contract: Identity { // (1)
associatedtype ID
func getID() -> ID
// ...
}
@reparentable // (2)
@available(monoidLib 9, *) // (3)
public protocol Identity { // (7)
associatedtype ID
associatedtype Validator = DefaultValidator // (4)
var id: ID { get }
func getValidator() -> Validator
}
public struct DefaultValidator {}
extension Contract: @reparented Identity // (7)
where Validator == DefaultValidator { // (5) & (8)
public var id: ID { getID() } (6)
public func getValidator() -> Validator {
return DefaultValidator()
}
}
- The inheritance clause of
Contract(the child) must still reflect that it inherits fromIdentity(the new parent). - The new parent is also a new protocol that will be born with
@reparentablefrom the outset. It's not safe to add (or remove) the@reparentableattribute on an existing protocol, as it can break ABI and source compatability for all clients that have defined their own protocols inheriting from it. - Availability may also be required on the new parent protocol, to match the version that protocol was introduced. Unlike traditional protocol inheritance, it's okay if the new parent protocol is less available than the child protocol.
- Much like introducing new requirements to a protocol, default implementations of all requirements in the new parent protocol must be made available even outside of the reparented extension, to avoid a source break when clients rebuild. In this case, a default type witness for Validator is provided in the Identity, but a restatement of the associatedtype requirement, with a default, can be put in Contract instead.
- The reparented extension can only consist of a where clause that contains same-type requirements that bind the associated types a concrete type (here
DefaultValidator). If kept generic, the associated types must have the same name (here, both are calledID, so it's an implicitID == ID). Thus, if Identity used the nameIdentfor its associated type requirement, writingID == Identin the reparented extension is not currently supported. - All other requirements of the new parent must have default implementations within scope of the reparented extension. Thus, if there are methods only conditionally available (i.e., within other extensions constrained by a
whereclause), they cannot be used to implement the requirements. - Reparentable protocols can only inherit from marker protocols like
Sendable. - You cannot conditionalize a reparented extension based on conformance requirements, e.g.,
where ID: Equatableis not permitted.
Gotchas
There are a few subtle aspects of reparenting to keep in mind.
Overhead
For any type conforming to a reparentable protocol, accessing the witness table for that reparentable protocol is less efficient; a relative offset amounting to an extra subtraction of two addresses must be computed. This has more overhead than a normal protocol, which uses a fixed offset.
Default Type Witnesses
In the Identity example, Validator is a new associated type requirement that has a default witness DefaultValidator. Suppose another resilient library downstream of your library has types conforming to Contract. If they rebuild against your library that introduces the new inheritance relationship, without declaring a different type to witness the Validator requirement for all Contract-conforming types, then those types will be forever stuck with DefaultValidator as part of their ABI.
Availability
Access to all requirements and associated types of the protocol become gated by availability. So, given some Contract, you could not call getValidator() or it cast to some Identity without making it conditional on availability of the protocol.
Overload Resolution
If your new parent protocol introduces requirements that have the same name as those mentioned in protocols now-inheriting from it, problems can ensue and you may need to use @_disfavoredOverload.
Overrides
All requirements in an inheritor of a reparentable protocol are implicitly treated as if they were @_nonoverride, relative to the reparentable protocol's requirements. In other words, the witness table entries of inheriting protocols are not merged together with identical requirements in a parent protocol, if that parent is declared @reparentable. Reparentable protocols must have their own witness table entries for all requirements.