Do we have to improve the compiler's look ahead here?

Here’s the prototype for my doubly-linked list (DLL) protocol:

public protocol TransferableCollection: PoorlyTransferableCollection where Self: BidirectionalCollection, Self.SubSequence: TransferableCollection

Since PoorlyTransferableCollection is a singly-linked list (SLL) protocol, it needs to work with half-closed ranges instead of our usual half-open ones (to have O(1) operations instead of O(n)). The DLL has versions of the SLL core methods, but they take Range<Index> instead of my custom InvertedRange<Index>. The DLL has default implementations of the SLL’s version that just convert & forward:

private func convertRange(_ range: InvertedRange<Index>) -> Range<Index> { /*...*/ }

private func convertRange<R>(_ range: R) -> InvertedRange<Index> where R: RangeExpression, R.Bound == Index {
    let r = range.relative(to: self)
    switch (r.lowerBound, r.upperBound) {
    case (startIndex, endIndex):
        return .all
    case (startIndex, startIndex):
        return .beforeStart
    case (startIndex, let i):
        return .prefix(upThrough: index(before: i))  // ERROR
    case (let i, endIndex):
        return .suffix(after: index(before: i))  // AND HERE
    case (endIndex, endIndex):
        return .afterEnd
    case (let s, let e):
        return .range(after: index(before: s), upThrough: index(before: e))  // AND HERE

But I got errors of:

Argument labels '(before:)' do not match any available overloads

until I changed my protocol’s header to:

public protocol TransferableCollection: PoorlyTransferableCollection, BidirectionalCollection where Self.SubSequence: TransferableCollection

It seems that the compiler can’t peek at where-clauses to help determine what interface Self must already have. Is this a bug? Or am I triggering something that shouldn’t be fixed due to causing other problems? There is a more definitive work-around, after all.

I wouldn’t call this “look-ahead”, but yes, it’s a bug—where clauses should be treated equivalently to inheritance. You’ve already got a nice, self-contained test case, so please file it!

In general for protocols, constraints on Self are handled differently than entries in the inheritance clause. Once we’ve fully type checked the protocol definition they’re equivalent, but sometimes we need to find inherited protocols before type checking, and this code is not as general as I’d like. I consider this a bug that we need to fix at some point.

1 Like

I realized that the block introducing those methods started with:

extension TransferableCollection {

I changed my preamble to the “Self: BidirectionalCollection” version, then changed the extension block’s preamble to:

extension TransferableCollection where Self: BidirectionalCollection {

But the error remained. I’ve put everything back to the working order, but I wanted to eliminate this test case.

In a protocol, protocol P where Self : Q is equivalent to protocol P : Q. Until the bugs are fixed you should always use the latter.

Done: SR-7348 (“Using symbols in protocol extensions doesn’t always check where-clauses for allowable symbols.”).

I’d like us to think a bit more before fixing this bug, because relying on type checking for name lookup introduces more dependencies into the type-checking problem that slow down compilation and make cyclic dependencies more common.


1 Like

Do we really need to fully check the requirement signature though? I think we could proceed in a hacky manner and walk the RequirementReprs looking for things conforming to Self. Maybe that’s too hacky though, but I recall we do something similar with extensions since people write code like extension MyProto where Self : SomeClass.

We could look for Self : Foo requirements in the extension’s where clause, but have to draw a line somewhere, e.g., extension MyProto where Self == Self.A, Self.A: SomeClass { } shouldn’t work with name lookup.


1 Like