Why Swift call too shallow here

Viewed 130

Bit academic, but look at this code

// Some protocol
protocol R : Equatable
{
}

// Some class
class E : R
{
    // Default implementation
    static func == ( lhs : E, rhs : E ) -> Bool
    {
        print ( "Comparing E ===" )
        return lhs === rhs
    }
}

// Some class with a string
class F : E
{
    let f : String

    init ( f : String )
    {
        self.f = f
    }

    // Copare strings
    static func == ( lhs : F, rhs : F ) -> Bool
    {
        print ( "Comparing F ==" )
        return lhs.f == rhs.f
    }
}

// Some generic container type
class G < T : R > : R
{
    let g : T

    init ( g : T )
    {
        self.g = g
    }

    // Compare
    static func == ( lhs : G, rhs : G ) -> Bool
    {
        print ( "Comparing G ==" )
        return lhs.g == rhs.g
    }
}

let f1 = F ( f : "abc" )
let f2 = F ( f : "abc" )

print ( "f1 == f2 ? \( f1 == f2 ) (expect true)" )

let g1 = G < F > ( g : f1 )
let g2 = G < F > ( g : f2 )

print ( "g1 == g2 ? \( g1 == g2 ) (expect true)" )

This gives

Comparing F ==
f1 == f2 ? true (expect true)
Comparing G ==
Comparing E ===
g1 == g2 ? false (expect true)

The code defines some protocol and then a class E that implements it and provides a generic comparator. Next comes F which overrides this comparator to compare the string f it houses. Finally some container class G that houses something that implements the protocol.

So far so good. Now let us compare stuff. Comparing to instances of F works as the correct comparator is used. However, comparing two container classes does not work and it seems Swift calls too shallow. It calls the generic implementation provided in E in stead of the deeper one overridden in F. If this was Objective-C it would make that deeper call, even if the lhs and rhs were different classes and then it would typically crash. Swift will not allow it, which is easy to see by creating another class that e.g. contains Int in stead of String but at the same time it is not using the correct comparator it seems.

EDIT

This makes it worse. The change below is that f1, f2 and g1 and g2 are defined more generally as being (or containing) superclass types E. Even though F overrides the comparator it is not used in any of the comparisons.

let f1 : E = F ( f : "abc" )
let f2 : E = F ( f : "abc" )

print ( "f1 == f2 ? \( f1 == f2 ) (expect true)" )

let g1 = G < E > ( g : f1 )
let g2 = G < E > ( g : f2 )

print ( "g1 == g2 ? \( g1 == g2 ) (expect true)" )

I suspect this has something to do with operator overloading vs. function overriding ... but any light will be appreciated.

EDIT 2

Toyed a bit with some thinking if I use it then maybe it could get this to work the way I want but some is not allowed for a function argument at present.

2 Answers

I've taken a look at the compiled output of a streamlined version of the above:

class E: Equatable {
  static func ==(lhs: E, rhs: E) -> Bool { return false }
}

class F: E {
  static func ==(lhs: F, rhs: F) -> Bool { return false }
}

class G <T: Equatable>: Equatable {
  let g: T

  init(_ g: T) {
    self.g = g
  }

  static func ==(lhs: G, rhs: G) -> Bool {
    return lhs.g == rhs.g
  }
}

and the missing link is the absence of a protocol witness for the inheriting class. The use of a protocol witness is detailed here.

If you take a look at the symbol table:

$ nm main | swift demangle | grep 'protocol witness'
00000001000024a0 t protocol witness for static Swift.Equatable.== infix(A, A) -> Swift.Bool in conformance main.E : Swift.Equatable in main
00000001000028e0 t protocol witness for static Swift.Equatable.== infix(A, A) -> Swift.Bool in conformance main.G<A> : Swift.Equatable in main

you can see one for E and G, but not for F.

If you modify the code to add conformance to Equatable, you unsurprisingly get the additional protocol witness:

$ nm main | swift demangle | grep 'protocol witness'
00000001000024a0 t protocol witness for static Swift.Equatable.== infix(A, A) -> Swift.Bool in conformance main.E : Swift.Equatable in main
00000001000025a0 t protocol witness for static Swift.Equatable.== infix(A, A) -> Swift.Bool in conformance main.F : Swift.Equatable in main
00000001000028d0 t protocol witness for static Swift.Equatable.== infix(A, A) -> Swift.Bool in conformance main.G<A> : Swift.Equatable in main

So, yeah it could be very strongly upheld that this is a bug, but as noted in a related bug report it's the way things are, so not likely to be changed without significant ceremony and process.

@msbit provided a great explanation of what happens under the hood, I'll try to tackle the problem from another angle.

Let's take a look at the protocol declaration:

public protocol Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool
}

As soon as a protocol uses Self in its declaration, or has associated types, it leaves the dynamic dispatch world, and lands in the static dispatch one. And this static dispatch gets into the way of dynamic method resolution via inheritance.

Another aspect is that the == method is a static requirement, and when it comes to classes, static members receive the static dispatch, instead of the dynamic one. This means that when referencing the derived class though a reference to the base class, the method call will always be dispatched to the base class.

You would need to use class func in order to get to the dynamic dispatch, however protocol declaration don't allow specifying requirements as class func, even if the protocol is a class type one.

Thus, the runtime behaviour is the correct one in this case, even is not the expected one, as it follows the language design. The problem I see is that the compiler allows you to re-implement without overriding a static method (== in this case), as this is not allowed for regular, non-operator, static methods.

class Base {
    static func sayMyName() {
        print("My name is Base")
    }

    static func ==(lhs: Base, rhs: Base) -> Bool {
        true
    }

    static func >>(lhs: Base, rhs: Base) -> Bool {
        true
    }
}

class Derived: Base {
    // this not allowed by the compiler
    static func sayMyName() {
        print("My name is Derived")
    }

    // this override is allowed, though `Base` doesn't conform
    // to Equatable
    static func ==(lhs: Derived, rhs: Derived) -> Bool {
        true
    }

    // Other operator-like static overrides are also allowed
    static func >>(lhs: Derived, rhs: Derived) -> Bool {
        true
    }
}
Related