Minor inference bug in identity operator

Looks like a bug, is it?

class Foo: Equatable {
    static func == (a: Foo, b: Foo) -> Bool {
        a.x == b.x
    }
    var x: Int = 0

    static let foo = Foo()
}

func foo() {
    _ = Foo() == Foo.foo  // ✅
    _ = Foo() === Foo.foo // ✅
    _ = Foo() == .foo     // ✅
    _ = Foo() === .foo    // ❌ Type 'AnyObject?' has no member 'foo'
}

Depends on what exactly you mean by "a bug". === is defined to allow you to compare any two AnyObject?s

public func === (lhs: AnyObject?, rhs: AnyObject?) -> Bool { ... }

by comparing their ObjectIdentifiers. The operator isn't written generically like ==, so the types of both the left and right hand sides are fixed at AnyObject?. Within that context, the diagnostic is correct (and behaves as expected).

1 Like

IIUC this is not a bug, and is expected behavior. The diagnostic maybe be a little bit misleading at first glance but if you look a little more on the definition of === in the standard library it does make sense.
What happens is that the only definition of === for equatable is public func === (lhs: AnyObject?, rhs: AnyObject?) -> Bool { so when we call as Foo() === Foo.foo the compiler already knows both types as being Foo and when it attempts this overload since Foo is a subtype of AnyObject this is a valid overload and the expression type checks as expected.
But in the case of Foo() === .foo the base type for unresolved member has to be inferred by the compiler from the overload argument type, but this the overload type is (AnyObject?, AnyObject?) -> Bool this is not enough contextual information for Foo to be inferred because at this overload attempt the compiler only knows AnyObject? to attempt as base type(that is the reason for the diagnostic).
It is basically the same as

  let a: AnyObject? = .foo // ❌ Type 'AnyObject?' has no member 'foo'

And also you can see that it would work if you defined an overload for === as static func === (a: Foo, b: Foo) -> Bool.

So TLDR; is that because the only overload of === is (AnyObject?, AnyObject?) -> Bool compiler only have "contextual" information to infer Base of <Base>.foo as AnyObject?.

1 Like

Could this be true: "Foo() === Bar()" where Foo and Bar are two different reference types?

I guess i could make some type casting so that the two differently typed foo & bar variables lay at the same address.

Let me clarify the followup question: If I do this:

Foo() === Baz.foo

Will the result of this be ever true? If yes - please provide an example. If no - then I'd probably never do this and when I write "Foo() === .foo" the intent of the expression should be clear to the compiler. methink :thinking:

A similar use case when compiler is not confused:

struct Foo {
    static let foo = Foo()
}

struct Bar {
    static let foo = Foo()
}

let foo1: Foo = .foo // no confusion to the compiler
let foo2: Foo = Bar.foo // albeit this is fine

Sure. The simplest example would be if one is a subclass of another, but you can also do it if one or both types are protocols. I see that it’d be convenient to infer the type sometimes, though.

2 Likes

I think this is the real kicker here. A generic identity check will work if Foo and Bar are related via inheritance, but not if either Foo or Bar are protocols:

func identical<T: AnyObject>(_ lhs: T?, _ rhs: T?) -> Bool {
    switch (lhs, rhs) {
    case (nil, nil): return true
    case let (.some(lhs), .some(rhs)): return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
    default: return false
    }
}

class FooClass {}
class BarClass: FooClass {}

let bC = BarClass()
let fC: FooClass = bC
print(fC === bC) // => true
print(identical(fC, bC)) // => true

// -------

protocol FooProto: AnyObject {}
protocol BarProto: AnyObject {}
class X: FooProto, BarProto {}

let x = X()
let fP: FooProto = x
let bP: BarProto = x
print(fP === bP) // => true
print(identical(fP, bP)) // Error: Type of expression is ambiguous without more context

(Theoretically, it might be possible to add a generic overload of === to allow type inference, but given the already exponential nature of type-checking overloaded operators elsewhere, it seems unlikely to be worth it.)


There's also another trivial case of Foo === Bar where Foo and Bar are concrete types not related by inheritance, but it's not particularly compelling:

class Foo {}
class Bar {}

let f = Foo()
let b = unsafeBitCast(f, to: Bar.self)
print(f === b) // => true