Compiler finding non-existing ambiguous candidates?

The following is a specific example of a situation where the compiler reports ambiguous candidates even though there are none.

Trying to compile this little program (with Xcode 9.3)

let a = [1, 2, 3]
let b = a.joined()

will result in the compiler error:

Ambiguous reference to member 'joined()'

Xcode’s UI is not able to show the candidates, but compiling via command line will show this

› swiftc test.swift
test.swift:2:9: error: ambiguous reference to member 'joined()'
let b = a.joined()
        ^
Swift.Sequence:2:17: note: found this candidate
    public func joined() -> FlattenSequence<Self>
                ^
Swift.Sequence:2:17: note: found this candidate
    public func joined<Separator>(separator: Separator) -> JoinedSequence<Self> where Separator : Sequence, Separator.Element == Self.Element.Element
                ^
Swift.Sequence:2:17: note: found this candidate
    public func joined(separator: String = default) -> String
                ^
Swift.Collection:2:17: note: found this candidate
    public func joined() -> FlattenCollection<Self>
                ^
Swift.BidirectionalCollection:2:17: note: found this candidate
    public func joined() -> FlattenBidirectionalCollection<Self>
                ^
Swift.BidirectionalCollection:2:17: note: found this candidate
    public func joined(separator: String = default) -> String

As far as I can tell, none of these candidates should actually be considered candidates, and the correct diagnostics should instead be:

Value of type '[Int]' has no member 'joined'

Or am I missing something?

Yes, that should be the correct diagnostics. This might be related to SR-7046. There are several precedents of incorrect or indirect diagnostics that I’ve come across recently. Ironically, the reason I haven’t reported them all is because once you encounter one several times you get used to it and understand the real meaning. But that’s certainly not the right approach :)

Here’s one of them

class A<T> {}

extension A where T == Bool {
    
    func foo() {}
}

let b = A<Int>()

b.foo() // 'A<Int>' is not a subtype of 'A<Bool>'

Although it is correct, I believe this to be too indirect. It definitely isn’t trivial at first sight that the receiver’s type has no member foo.

2 Likes

We don’t want to just say “value of type ‘[Int]’ has no member ‘joined’” because a user might say “wait, but Array has all these different joined methods! they’re right there!” That said, the diagnostic shouldn’t say this is ambiguous; it should say something like "none of the overloads for joined work on your type, which is [Int]". And then the notes can (ideally) say what doesn’t match about each one.

(C++ diagnostics get a lot of flak, but at least they do this much and not just “found this overload”!)

I think there’s already an SR about this but I can’t find it offhand.

3 Likes

Everything Jordan said, with the additional caveat that in general, confusing or inaccurate diagnostics are a result of the constraint solver returning a single “yes” or “no” answer when asked to type check; a “no” answer then triggers a separate diagnostics pass, which tries to type check individual sub-expressions of the larger expression until it finds some pieces that don’t fit together. This process is inherently lossy and sometimes it can get confused itself (this is also why you see errors like “cannot convert Int to Int”, etc, although we may have fixed the most egregious cases of that).

What nostalgia.. :)

IMG_4197

2 Likes

IMO I feel the “fixing” of the error message that said I couldn’t apply + to two ‘Int’ types is a severe regression. It really helped brighten the mood seeing things like that :).

1 Like