swiftlang / swift

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

[SR-7928] Explain and/or fix strange behavior of protocol extensions #50463

Open jepers opened 6 years ago

jepers commented 6 years ago
Previous ID SR-7928
Radar None
Original Reporter @jepers
Type Bug
Environment Xcode 9.4, default toolchain and recent snapshots Xcode 10, default toolchain and recent snapshots (2018-06-05)
Additional Detail from JIRA | | | |------------------|-----------------| |Votes | 0 | |Component/s | Compiler | |Labels | Bug, DiagnosticsQoI | |Assignee | None | |Priority | Medium | md5: ea85cc6b4e13a3635f0d0c391347d71f

Issue Description:

First an example involving constrained protocol extensions:

protocol P {
    func foo()
}
protocol Q {}

extension P {
    func foo() {
        print("The `foo()` defined in `extension P`")
    }
}

extension P where Self: Q {
    func foo() {
        print("The `foo()` defined in `extension P where Self: Q`")
    }
}
extension Q where Self: P {
    func foo() {
        print("The `foo()` defined in `extension Q where Self: P`")
    }
}

struct S: P {}

// This program compiles succesfully in it's current state (accepts invalid?).

// But if we uncomment the following line:
// extension S: Q {}

// we will get a compile time error for the line `struct S: P {}` saying that:
// Type 'S' does not conform to protocol 'P'

And a simpler but similar example:

protocol P {
    func foo()
}
protocol Q {}

extension P {
    func foo() {
        print("The `foo()` defined in `extension P`")
    }
}

extension Q {
    func foo() {
        print("The `foo()` defined in `extension Q`")
    }
}

struct S: P {}

// This program compiles succesfully in it's current state (as expected).

// But if we uncomment the following line:
// extension S: Q {}

// we will get a compile time error for the line `struct S: P {}` saying that:
// Type 'S' does not conform to protocol 'P'
// And if I press the stop-icon, and look in the Issue Navigator, there
// are more errors stating that there are multiple matching functions
// so I guess this is OK except for Xcode focusing on the least
// relevant error message, and I have to click my way to the
// errors that say why S does not conform to P.
// I do wonder however what would be the case if S was defined
// in a separate module, and I added the conformance of S to Q
// in another module (as it wouldn't be possible to show the error
// on the declaration of S then ...).

And a variant of the simple example showing that what wasn't allowed above, is allowed if we make S generic, so for example the following compiles successfully:

protocol P {
    func foo()
}
protocol Q {}

extension P {
    func foo() {
        print("The `foo()` defined in `extension P`")
    }
}

extension Q {
    func foo() {
        print("The `foo()` defined in `extension Q`")
    }
}

struct S<T>: P {}

extension S: Q where T: SignedInteger {}

// And we can call foo(), but only on an existential:

let s = S<Int>()
// s.foo() // ERROR: Ambiguous use of 'foo()'

let sp: P = S<Int>()
sp.foo() // The `foo()` defined in `extension P`

let sq: Q = S<Int>()
sq.foo() // The `foo()` defined in `extension Q`

let spq: P & Q = S<Int>()
spq.foo() // The `foo()` defined in `extension P`

let sqp: Q & P = S<Int>()
sqp.foo() // The `foo()` defined in `extension P`

// Perhaps this is as designed?
// But if so, why does S have to be generic for it to work?

The first example also works if S is generic:

protocol P {
    func foo()
}
protocol Q {}

extension P {
    func foo() {
        print("The `foo()` defined in `extension P`")
    }
}

extension Q {
    func foo() {
        print("The `foo()` defined in `extension Q`")
    }
}

extension P where Self: Q {
    func foo() {
        print("The `foo()` defined in `extension P where Self: Q`")
    }
}
extension Q where Self: P {
    func foo() {
        print("The `foo()` defined in `extension Q where Self: P`")
    }
}

struct S<T>: P {}

extension S: Q where T: SignedInteger {}

let s = S<Int>()
// s.foo() // ERROR: Ambiguous use of 'foo()'

let sp: P = S<Int>()
sp.foo() // The `foo()` defined in `extension P`

let sq: Q = S<Int>()
sq.foo() // The `foo()` defined in `extension Q`

let spq: P & Q = S<Int>()
spq.foo() // The `foo()` defined in `extension P`

let sqp: Q & P = S<Int>()
sqp.foo() // The `foo()` defined in `extension P`
jepers commented 6 years ago

There's this notes in TSPL 4.2 saying

"If a conforming type satisfies the requirements for multiple constrained extensions that provide implementations for the same method or property, Swift uses the implementation corresponding to the most specialized constraints."

Perhaps my example programs have multiple implementations corresponding to equally specialized constraints?

belkadan commented 5 years ago

Clearing up old bugs. Yes, your intuition was correct: the compiler considers the various overloads of foo equally likely in the normal cases, but once you make the conformance of Q conditional, the implementations of foo that depend on being a Q aren't valid as a choice for the unconditional P. What do you think we can do to make this clearer?

jepers commented 5 years ago

I think that question has to be answered by someone more knowledgeable than me, assuming they too think this is unexpected behavior. Please let me know if you think I should just close the bug.