swiftlang / swift

The Swift Programming Language
https://swift.org
Apache License 2.0
67.33k stars 10.34k forks source link

[SR-5421] Can't use same case in two enums #47995

Closed swift-ci closed 7 years ago

swift-ci commented 7 years ago
Previous ID SR-5421
Radar rdar://problem/33226924
Original Reporter smartgo (JIRA User)
Type Bug
Status Resolved
Resolution Invalid
Environment Xcode 9.0 beta 3 (9M174d), both when compiled as Swift 3.2 and Swift 4.0.
Additional Detail from JIRA | | | |------------------|-----------------| |Votes | 0 | |Component/s | Compiler | |Labels | Bug, 4.0Regression | |Assignee | None | |Priority | Medium | md5: 7001de01ec0f975646ce9040af0d79b9

Issue Description:

The following code shows a bug in Xcode 9.0 beta 3 (9M174d) that was not there in beta 2. The references to .black and .white are marked as errors, as it doesn't know whether to use the one in BlackWhite or the one in EmptyBlackWhite:
Ambiguous use of 'black'

When commenting out the comparison functions, the error disappears.

import Foundation

enum BlackWhite: Int {

    case black = 1
    case white = 2

    init(_ color: EmptyBlackWhite) {
        assert(color == .black || color == .white)
        self = BlackWhite(rawValue: color.rawValue)!
    }

    var isBlack: Bool { return self == .black }
    var isWhite: Bool { return self == .white }
}

enum EmptyBlackWhite: Int {

    case empty = 0
    case black = 1
    case white = 2

    init(_ color: BlackWhite) {
        assert(color == .black || color == .white)
        self = EmptyBlackWhite(rawValue: color.rawValue)!
    }

    var isBlackWhite: Bool { return self == .black || self == .white }
}

// Comment out the following lines, and the bug disapears.
func == (lhs: BlackWhite, rhs: EmptyBlackWhite) -> Bool {
    return lhs.rawValue == rhs.rawValue
}

func != (lhs: BlackWhite, rhs: EmptyBlackWhite) -> Bool {
    return lhs.rawValue != rhs.rawValue
}

func == (lhs: EmptyBlackWhite, rhs: BlackWhite) -> Bool {
    return lhs.rawValue == rhs.rawValue
}

func != (lhs: EmptyBlackWhite, rhs: BlackWhite) -> Bool {
    return lhs.rawValue != rhs.rawValue
}
d4adc30e-df4b-4925-bfeb-69e631c0be69 commented 7 years ago

This is expected behavior: the compiler cannot tell that the two blacks and the two whites are semantically identical, and so needs to unambiguously choose one of them whenever those names are referenced. By default, enums with raw values automatically get == comparisons with their own type, meaning the following functions exist:

func == (lhs: BlackWhite, rhs: BlackWhite) -> Bool {
    // ...
}
func != (lhs: BlackWhite, rhs: BlackWhite) -> Bool {
    // ...
}
func == (lhs: EmptyBlackWhite, rhs: EmptyBlackWhite) -> Bool {
    // ...
}
func != (lhs: EmptyBlackWhite, rhs: EmptyBlackWhite) -> Bool {
    // ...
}

With just these implicit functions, references like `someBlackWhite == .black` or `someEmptyBlackWhite != .black` is unambiguous: for the first case, `someBlackWhite` has type `BlackWhite` and so the only `==` operator that can be used is the first one above, this forces the second argument to also have type `BlackWhite` and thus `.black` must be a member of this type. Similarly, the second example can only use the last `!=` operator and so the second argument is also forced to be `EmptyBlackWhite`.

With the new operators, there's no longer a single choice when the type of the first argument is chosen. For `someBlackWhite == .black`, this could refer to either

func == (lhs: BlackWhite, rhs: BlackWhite) -> Bool
func == (lhs: BlackWhite, rhs: EmptyBlackWhite) -> Bool

And the compiler has no way to work out which you wanted.

A good way to fix this is to not have two separate types, and, assuming the example is close to your real code, the correct way to add an "empty" case is to use `Optional`. Instead of `EmptyBlackWhite`, one can write `BlackWhite?`, with `nil` being `.empty`. This is much "Swiftier" than having two separate enums, and also works nicely with operators like == due to careful overloads in the standard library, e.g. all of the following compile and behave as one might expect (and so do all the various permutations/combinations)

let x: BlackWhite = ...
let y: BlackWhite? = ...

x == y
x == .black
y == .black
y == nil
swift-ci commented 7 years ago

Comment by Anders Kierulf (JIRA)

Thanks for the explanation, makes sense; the bug was that this worked before, not that it is no longer working.

Using optional BlackWhite instead of EmptyBlackWhite is a good suggestion that will work in some cases where I'm using EmptyBlackWhite, but not in general, as it also includes a fourth case (blackAndWhite) that's used in low-level bitboards (for the game of Go).

d4adc30e-df4b-4925-bfeb-69e631c0be69 commented 7 years ago

Oh, I apologize, I completely missed that it was a difference between versions. I can reproduce it, and I have no idea what's going on.

@swift-ci create

rudkx commented 7 years ago

The current behavior looks correct to me, although I do not know what changed.

You can always disambiguate by using the full name rather than just the dot followed by member.

rudkx commented 7 years ago

I took a closer look at this. We used to compile this without error due to a hack in the type checker that would stop looking at overloads under some conditions. That hack was narrowed, which is why we are now (correctly) diagnosing this as ambiguous.