I’ve been able to move this idea forward to reach its theoretical conclusion. I’ve raised new PR and this time an evolution proposal.
This final version has a limitation (which I’ll discuss below) but with the prototype toolchain it is possible to do pretty much anything in the way of retroactively refining protocol conformances as mentioned in the generics manifesto.
To understand the limitation it necessary to recap how protocols are represented at run time and what are these “witness tables”.
An existential container is a 5 (64 bit) word struct in the C sense that represents a reference to a nominal (class or struct) conforming to a protocol i.e. if you define a function that takes a protocol as an argument, this container is what is actually passed to the function. A "witness table” (a pointer to which is contained in this structure) is a minimal representation of the information needed at runtime to dispatch calls to protocol members onto the nominal (or protocol extension)’s implementation. In the prototype toolchain they have the following slightly modified structure:
Nominal_Portocol_WP:
* associated type entries
* pointers to the witness tables of directly inherited protocols
* pointer to “FTW” member function thunks of the original protocol for the nominal
* witness tables of "extended conformances" in order of the module that added the conformance.
The last entry(s) is where the witness table has been extended and is at the end so the table is compatible with functions that are declared in modules that are unaware of any extended conformances. In concrete terms, say you have the following code in “ModuleA":
public struct A {
public init() {}
let string = "Hello Swift"
}
public protocol P {
func foo()
}
extension A: P {
public func foo() {
print("\(self)")
}
}
With the PR, it would now be possible to put the following in another ModuleQ:
import ModuleA
public protocol Q {
func qoo()
}
public extension P: Q {
func qoo() {
print("\(self)")
}
}
Meanwhile back in the project's main module you can now write:
import ModuleA
import ModuleQ
func something() {
A().qoo()
}
This is the essence of the idea. New extended witness tables are generated for all nominal types referred to to in the source file for protocol P, all well and good.
Without wanting to emphasise a shortcoming of this model the worst case is where there is a function such this in Module A.
public func ap() -> P {
return A()
}
Back in the main module the following will compile no problem:
ap().foo()
ap().qoo()
The first call is fine but the second call will crash as the essential container returned by the function ap() in ModuleA contains a version of the witness table that does not contain the extended conformance to protocol Q. This is a reliable crash like a force unwrap and not exactly "undefined behaviour" but it doesn't give any useful diagnostic. Whether in practice this amounts to a common case is up for discussion but that’s the situation. Provided the function ap() is defined in a module that imported ModuleQ there isn't a problem.
Meanwhile, as these ad-hoc extended witness tables also emit protocol conformance descriptors, dynamic casting works without having to change the existing runtime. For example, if the following is called with an instance of A():
func anything(a: Any) {
(a as? Q)?.qoo()
}
This is about as far as I can take the idea and for me it is worth a punt given its power and the changes to the compiler are relatively minor and strictly additive. There are no ABI issues I’m aware of.