Crash with Array unsafeUninitializedCapacity and tuples

Can anyone see why this code would crash. I'm thinking it is the compiler generating bad code but wanted to verify before adding a bug to swiftlang.

func tuples(_ values: [String]) -> [(String, String)] {
    let count = values.count / 2
    return .init(unsafeUninitializedCapacity: count) { buffer, count in
        var iterator = values.makeIterator()
        while let value1 = iterator.next() {
            guard let value2 = iterator.next() else { return }
            buffer[count] = (value1, value2)
            count += 1
        }
    }
}
print(tuples(["Hello", "ert", "sdf", "fgh"]))

The code runs fine on macOS but crashes on Ubuntu Noble

1 Like

(String, String) is a nontrivial object; since buffer is uninitialized when the closure begins, you have to use:

buffer.initializeElement(at: count, to: (value1, value2))

instead of

buffer[count] = (value1, value2)

in order to initialize it. You can avoid this by restricting yourself to safe Swift:

func tuples(_ values: [String]) -> [(String, String)] {
    (0 ..< values.count/2).map { i in (values[2*i], values[2*i+1]) }
}

or possibly by using chunks(ofCount:) from Swift Algorithms, though this has slightly different behavior when there are an odd number of elements in values.

7 Likes

The unfortunately nonobvious issue here is that you are operating on uninitialized memory, but using MutableCollection API. UnsafeMutableBufferPointer's collection API is only valid for initialized memory.

We are proposing an API addition where the uninitialized memory is represented by an OutputSpan:

extension Array {
  public init<E: Error>(
    capacity: Int,
    initializingWith initializer: (inout OutputSpan<Element>) throws(E) -> Void
  ) throws(E)
}

With this initializer, the example above would be:

func tuples(_ values: [String]) -> [(String, String)] {
    let count = values.count / 2
    return .init(capacity: count) { span in
        var iterator = values.makeIterator()
        while let value1 = iterator.next() {
            guard let value2 = iterator.next() else { return }
            span.append((value1, value2))
        }
    }
}
print(tuples(["Hello", "ert", "sdf", "fgh"]))
3 Likes

Ah that makes complete sense, thank you. Is there no way we could get a diagnostic to recognise this as an issue? Although if we can get a version of Array(unsafeUninitializedCapacity:) that uses OutputSpan I'd probably move to that in the future.

That’s the problem with unsafe pointers: the compiler is unable to help with their many states. There might be a way to improve this, but the effort is better spent on expanding the reach of safe abstractions.

3 Likes

It might be worth making the unsafeUninitializedCapacity: methods fill the memory with a somewhat unique sentinel value (endless screaming, 0xaaaa...aaaa) in debug builds, but yeah, we really want to move people off of these and onto OutputSpan, and just define most of these problems away.

1 Like

Tangentially, are we expecting to provide a safe version of unsafeTemporaryAllocation(of:capacity:) that provides an OutputSpan? Because I would be very interested in such a thing.

There's a compiler issue that prevents us from providing typed-throws variants of the withUnsafeTemporaryAllocation() functions, and that issue made me discount the idea of pursuing the safe withTemporaryAllocation(). However, that's wrong! We could and should implement a saferthrows/rethrows version and generalize it later when the compiler supports it.

2 Likes