Specifically, since B gets the default implementation of foo() from A, C cannot override that implementation with its own — in this case, C.foo() is not an override of B.foo() (or A.foo()) but a separate method with the same name. It happens to be able to call a superclass method with the same name (hence super.foo()), but it's not the same method.
So the fact that you can call super.foo() here isn't a bug, but the overall behavior could be considered to be (though at almost 10 years in, I suspect that this is going to be difficult to address at this point).
The "fix" is not implementable without a full objc_msgSend-style mechanism, because otherwise there is nowhere to put the vtable entry. The base class, conformance, and subclass can all be in different modules:
// first module:
open class C {}
// second module:
import FirstModule
public protocol P {
func hasDefault()
}
extension P {
public func hasDefault() {}
}
extension C: P {}
// third module:
import FirstModule
import SecondModule
class D: C {
override func hasDefault() { ... }
}
It's kind of like the "any P does not conform to P" situation, the bug is there to collect dupes but it's not something that can be addressed without completely changing the runtime model of the language.
Out of curiosity, as someone unfamiliar with how this is represented in the ABI — what do these type's vtables and witness tables look like in the case of
protocol P { func hasDefault() }
extension P { func hasDefault() {} }
class A1: P {}
class B1: A1 {}
class A2: P {
func hasDefault() {}
}
class B2: A2 {
override func hasDefault() {}
}
class B3: A2 {}
Is it accurate to say that
A1 has no vtable entry for hasDefault + its witness table entry points to the default impl
B1 is the same as A1
A2 has a vtable entry for hasDefault + its witness table entry points to that impl
B2 has a vtable entry for hasDefault (its overridden impl) + its witness table entry points to that overridden impl
B3 has a vtable entry for hasDefault (pointing to A2) + its witness table entry points to A2's impl
or is my understanding here flawed? (And am I totally bungling up the terminology here? )
I think using protocol defaults on a non final class should be a warning, though that is almost certainly over-diagnosis if subclasses aren't expected to override the defaults. You should at least be able to do @DefaultsOverridable, @DefaultsFinal, where overridable copies protocol defaults into its own vtable. Also, shadowing a protocol default should definitely be a warning.
Almost. The witness table is shared by all subclasses, so the witness table entry then performs a second dynamic dispatch through the class's vtable in that case.
In the early days of Swift, we had vague dreams of implementing a fallback msgSend-style dispatch mechanism, which would allow for class and protocol extensions to add new overridable members from outside of the original module's implementation. I wonder if even today we could retrofit such a mechanism by injecting the open dispatch table as a new vtable entry using our stable vtable ABI (setting aside the fact that it would be source-breaking to retroactively make extension methods dynamically dispatched).
Ah, thanks! That's what I was missing. Presumably, then, the witness table for A1 (and thus B1) says "no vtable entry, statically dispatch to the default"?
If so, then naively: would it be feasible to emit a hybrid witness table that effectively says "check the vtable for a method entry; if it exists, use it, otherwise fall back to the default", or are we limited by runtime metadata/ABI metadata/performance impact/something else? (Or is this effectively the msgSend-type interface we're talking about?)
The vtable for a class is emitted once, we can't add entries dynamically or look them up in any way (except using a fixed offset).
So if something like extension C: P {} uses a default implementation, there isn't much we can do except directly call to it from the witness thunk.
If the conformance is in the same source file as the class, we could conceivably add these special vtable entries when we emit the metadata for the class. But now you've got a new edge case to worry about, which is that this still doesn't work across files or modules
Ah, okay. I was thinking we might have enough runtime metadata about a class to know what that offset would be (if it exists) but sounds like that's not possible.