Conforming correctly to RangeReplaceableCollection

I am trying to conform to RangeReplaceableCollection. Although the code causes the program to freeze for a bit and then fails without an error. Here is a stripped-down minimal test implementation:

typealias Thing = String

final class CustomList {
    var things: [Thing]
    
    required init(things: [Thing]) {
        self.things = things
    }
}

extension CustomList: RangeReplaceableCollection {
    public convenience init() {
        self.init(things: [])
    }
    
    public var startIndex: Int { things.startIndex }
    public var endIndex: Int { things.endIndex }

    public subscript(key: Int) -> Thing {
        get { return things[key] }
    }
    
    public func index(after: Int) -> Int {
        return things.index(after:after)
    }

    func replaceSubrange(_ subrange: Range<Int>, with newElements: [Thing]) {
        things.replaceSubrange(subrange, with: newElements)
    }
}

var list = CustomList()

// This fails during runtime:
list += ["thing"]
// This too: list += CustomList(["thing"])

LLDB says:

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=2, address=0x7ffeef3ffff8)
    frame #0: 0x00007fff2ca311cc libswiftCore.dylib`swift_getAssociatedTypeWitness + 12
libswiftCore.dylib`swift_getAssociatedTypeWitness:
->  0x7fff2ca311cc <+12>: pushq  %rbx
    0x7fff2ca311cd <+13>: pushq  %rax
    0x7fff2ca311ce <+14>: movq   %rcx, %r12
    0x7fff2ca311d1 <+17>: movq   %r8, %rax

Not sure what I am doing wrong. What did I miss?

EDIT: Swift version:

swift-driver version: 1.26.9 Apple Swift version 5.5 (swiftlang-1300.0.31.1 clang-1300.0.29.1)
Target: x86_64-apple-macosx11.0

This is not the protocol requirement.

The requirement is generic and can take any collection with the proper element type.

• • •

I find it is easiest to build up conformances piece by piece.

First conform your type to Sequence in an extension.

Once the Sequence conformance works, then add another extension with Collection conformance.

When you have that working, add another extension for RangeReplaceableCollection, and so forth.

• • •

Also, as a style note, “after” does not make a good internal parameter name. You should add a better name, for example: func index(after i: Int) -> Int.

Wow, it's amazing that that compiles. Definitely file a bug.

The problem is that your signature for replaceSubrange is not correct. The compiler should complain that your type doesn't conform to RRC and guide you to a solution, but for some reason it builds it anyway and produces an invalid program.

The actual requirement is:

mutating func replaceSubrange<C>(_ subrange: Range<Self.Index>, with newElements: C) where C : Collection, Self.Element == C.Element

Note that newElements is a generic collection, not necessarily an Array. If we replace your function signature with this version, it works:

func replaceSubrange<C>(_ subrange: Range<Index>, with newElements: C) where C : Collection, C.Element == Thing {
  things.replaceSubrange(subrange, with: newElements)
}

var list = CustomList()
list += ["thing"]
list.things // ["thing"]
1 Like

Now I see. The generic signature resolved the problem.

I will file a bug for the code that should not compile.

Thank you!

1 Like

The root cause of your issue is [SR-6501] RangeReplaceableCollection default implementations cause infinite recursion · Issue #49051 · apple/swift · GitHub, and it is fixed on main.

(Forgive the elided where clauses in the following.) The issue is that a RangeReplaceableCollection must implement replaceSubrange<C: Collection>(_: Range<Index>, with: C), but RRC also has this convenience overload: replaceSubrange<R: RangeExpression, C: Collection>(_: R, with: C). This convenience overload translates the RangeExpression to a Range, then calls into the “must implement” version. Unfortunately, a Range is also a RangeExpression, and in the absence of the Range version of the function, we get a recursive call instead. It compiles, but the stack overflows at runtime.

It took a while for us to decide on the right fix for this situation. (There were a few of these in the standard library.) We have now added a number of default protocol function implementations that are marked @available(*, unavailable), so that these recursions become compilation errors.

10 Likes

Was this @available(*, unavailable) trick also done for the RandomNumberGenerator.next() method? Because that triggered the same recursive implementation bug with the version that's generic over the result type.

Yes. Also, the RNG fix is included in Swift 5.5.

1 Like