Hello everyone,
In an effort to formalize Swift's type system, I am trying to understand how Swift's member lookup strategy. I failed to find any sort of documentation for that (if there is, I would be very grateful if you could point me to it), so I conducted some tests.
Here is a description of the strategy I observed. Because Swift supports overloading, I assume the type solver has to collect all possible candidates first. I focused solely on method name resolution, based on the (possibly wrong?) assumption the strategy would work identically for properties. I also elided module imports and generic types.
For protocols:
- Add all method requirements with the requested name.
- Add all default implementations with the requested name that are declared in any of my extensions.
- For each of the protocols to which I conform, add any method that has a default implementation in an extension.
For structs and enums:
- Add all methods with the requested name that are declared in my own declaration.
- Add all methods with the requested name that are declared in any of my extensions.
- For each of the protocols to which I conform, add any method that has a default implementation in an extension, unless there is already a method in my own declaration or any of my extensions with the same signature.
For classes:
- Add all methods with the requested name that are declared in my own declaration.
- Add all methods with the requested name that are declared in any of my extensions.
- For each of my superclasses
S
, from my parent to my oldest ancestor, add all methods with the requested name that are declared inS
or any of its extensions, unless there is already a method with the same signature in the set. - For each of the protocols to which I conform (including the ones I inherited from my superclasses), add any method that has a default implementation in an extension, unless there is already a method in my own declaration, in any of my extensions, in any of my superclasses' declarations or any of their extensions with the same signature.
I guess this looks pretty much like anyone would have expected, except for default implementations collected from conformed protocols in class hierarchies. There, it appears that class inheritance does not matter and that all protocol conformances are treated as if they had been declared directly in the class. For instance, the code below won't compile (Swift 5.1.2), complaining about an ambiguous use of foo()
at the last line. My understanding is that the P
's default implementation is not overloaded in C
. Hence the error is the same as if I had declared C
's conformance to P
directly at C
's declaration. Of course, uncommenting either of foo
's declarations in A
, B
or C
makes the type checker happy.
protocol P {
func foo()
}
extension P {
func foo() { print("C.foo") }
}
protocol Q {
func foo()
}
extension Q {
func foo() { print("Q.foo") }
}
class A {
// func foo() { print("A.foo") }
}
class B: A, P {
// func foo() { print("B.foo") }
}
class C: B, Q {
// func foo() { print("C.foo") }
}
C().foo()
It also appears that method requirements do not impact the lookup strategy.
I would be grateful if someone could tell me if these observations are correct, or if I got anything wrong or incomplete.