Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SR-8022] Unexpected call to parent protocol requirement in protocol extension of conditional conformance #50555

Closed
stephencelis opened this issue Jun 17, 2018 · 11 comments
Assignees
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself

Comments

@stephencelis
Copy link
Contributor

Previous ID SR-8022
Radar rdar://problem/41216424
Original Reporter @stephencelis
Type Bug
Status Resolved
Resolution Invalid
Environment

Swift 4.1.5/4.2 (Xcode 10 beta)

Additional Detail from JIRA
Votes 0
Component/s Compiler
Labels Bug
Assignee @stephencelis
Priority Medium

md5: cab74402e124021f0f7a1439c4b68684

relates to:

  • SR-8357 Warn on methods in protocol extensions that are not defaults for requirements of the protocol but would be defaults of some parent protocol

Issue Description:

Enter the following code into a playground:

struct A<C: Collection>: Collection {
  let c: C
  var startIndex: C.Index { return c.startIndex }
  var endIndex: C.Index { return c.endIndex }
  subscript(position: C.Index) -> C.Element { return c[position] }
  func index(after i: C.Index) -> C.Index { return c.index(after: i) }
}

extension A: BidirectionalCollection where C: BidirectionalCollection {
  func index(before i: C.Index) -> C.Index { return c.index(before: i) }
}

A(c: "Hello").suffix(2)

And you get this runtime error on the final line:

Fatal error: Only BidirectionalCollections can be advanced by a negative amount
@belkadan
Copy link
Contributor

@swift-ci create

@moiseev
Copy link
Mannequin

moiseev mannequin commented Jun 29, 2018

It looks to me to be an instance of a problem described here: https://github.com/apple/swift-evolution/blob/master/proposals/0143-conditional-conformances.md#overloading-across-constrained-extensions

The bidirection-ness of String gets lost inside the suffix(_: ) overload provided by the extension BidirectionalCollection. Something that needs to be worked around like here, for example,

public func index(
_ i: Index, offsetBy n: Int, limitedBy limit: Index
) -> Index? {
var i = i
let step = n.signum()
// The following line makes sure that index(_:offsetBy:limitedBy:) is
// invoked on the _base at least once, to trigger a _precondition in
// forward only collections.
_ensureBidirectional(step: step)
for _ in 0 ..< abs(numericCast(n)) {
if i == limit {
return nil
}
_advanceIndex(&i, step: step)
}
return i
}
.

So if anything, this requires a compiler change. Moving to the Compiler component and /cc @DougGregor and @huonw to maybe explain why this is not going to happen.

@huonw
Copy link
Mannequin

huonw mannequin commented Jul 19, 2018

The bidirection-ness of String gets lost inside the suffix(_: ) overload provided by the extension BidirectionalCollection. Something that needs to be worked around like here, for example,

I don't think that makes sense as a purposeful choice on our part. The extension BidirectionalCollection as applied to A: BidirectionalCollection should be working with A<...> as an (essentially) opaque entity that is known to conform to BidirectionalCollection, it shouldn't need to be worrying about the type parameter/conditional bounds at all. Thus, this is either a bug in the compiler, or a bug in how suffix (etc.) is written (or both), because this definitely should work! As you say, the code is correctly picking up BidirectionalCollection.suffix, meaning it knows that it’s working with an type that is a bidirectional collection and so that should mean it behaves just like a nonconditionally-conforming type.

Reduced:

public protocol Collection2 {
    // reduced form of: func index(_ i: Index, offsetBy n: Int, limitedBy limit: Index) -> Index?
    func index()
}

extension Collection2 {
    public func index() {
        print("bad")
    }
}

public protocol BidirectionalCollection2: Collection2 {
    // func index()
}

extension BidirectionalCollection2 {
    public func suffix() {
        index()
    }

    public func index() {
        print("good")
    }
}

struct X: BidirectionalCollection2 {}
struct A<C: Collection2>: Collection2 {}
extension A: BidirectionalCollection2 where C: BidirectionalCollection2 {}

A<X>().suffix() // "bad"

One way to work around this is to uncomment the func index() requirement (i.e. add the Collection index(...) requirement to BidirectionalCollection). Another alternative for this specific case in the standard library would be for suffix to call _index instead of index (which is just a public wrapper that dispatches to _index).

@huonw
Copy link
Mannequin

huonw mannequin commented Jul 19, 2018

I opened #18066 with the latter work-around... whether we want to merge it is up to the stdlib team. I'm personally probably not going to be able to look at the compiler in the near future.

@rudkx
Copy link
Member

rudkx commented Jul 24, 2018

Here's an example demonstrating what's going on here without using constrained extensions. The behavior is expected.

It probably makes sense to emit some kind of warning on the definition of f() in Q since what's happening here is pretty subtle. Having said that, it's not clear how you would silence this warning if you really intend to have a function with the same signature that doesn't have a corresponding requirement in the protocol it is extending (where that requirement exists in one of the protocols this protocol extends).

protocol P {
  func f()
}
extension P {
  func f() { print("P") }
}

protocol Q : P {
  // func f() // uncomment to generate a witness table entry for Q.f
}
extension Q {
  func f() { print("Q") }

  func test() {
    f() // dynamic dispatch through witness
  }
}

class X : P {}
class Y : X, Q {}

Y().test() // prints P rather than Q since test() calls f() through dynamic dispatch rather than statically resolving it to Q’s f()

@rudkx
Copy link
Member

rudkx commented Jul 24, 2018

Relate to new bug for suggested warning: https://bugs.swift.org/browse/SR-8357

@huonw
Copy link
Mannequin

huonw mannequin commented Jul 24, 2018

It surprises me that this is intended behaviour; is there a reason we're not going to try to make this code work as most people would expect (I would think)?

@belkadan
Copy link
Contributor

That could also be incorrect, if X had its own implementation of f that was more specific than either P's or Q's.

@rudkx
Copy link
Member

rudkx commented Jul 24, 2018

@huonw I agree that behavior here is a bit surprising and I am not sure exactly what the reasoning behind this except perhaps that you can only provide default implementations in an extension for the stated requirements of the protocol (as opposed to inherited ones). Perhaps @DougGregor could comment on that?

@belkadan I'm not sure I follow what you're saying, but if you add an implementation to X, we'll call that implementation through dynamic dispatch even if Q has f as a stated requirement.

@belkadan
Copy link
Contributor

Sorry, that was my point. It's important that Q's extension call X's implementation of f even if it has its own f around.

We just had a very similar discussion in PR #17995.

@rudkx
Copy link
Member

rudkx commented Jul 26, 2018

@xedin and I were talking with @DougGregor about this and he pointed out an aspect of this that I had overlooked. The reason the `f()` in `Q` isn't called here is actually because of the concrete types involved. Since `X` conforms to `P`, the default implementation in `P` is used in `Y`'s witness for `f`. If you remove that conformance from `X`, then when we produce `Y`'s witness table we find `Q`'s `f()` as a default to fulfill the requirement of `f` on `P`, and end up calling `Q`'s `f()` from `Y().test()`.

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself
Projects
None yet
Development

No branches or pull requests

3 participants