Is it possible to capture a `borrowing` parameter in a non-escaping closure?

I'm trying to do something like this:

struct Foo {
  @inlinable
  public init(
    _ source: borrowing some RandomAccessCollection<Int>
  ) {

    _withUnprotectedUnsafeTemporaryAllocation(
      of: Int.self, capacity: source.count
    ) { buffer in
      let _ = source.startIndex
    }
  }
}

And I think it should be fine, but the compiler complains that the closure parameter to _withUnprotectedUnsafeTemporaryAllocation consumes source:

<source>:5:7: error: 'source' is borrowed and cannot be consumed
 3 |   @inlinable
 4 |   public init(
 5 |     _ source: borrowing some RandomAccessCollection<Int>
   |       `- error: 'source' is borrowed and cannot be consumed
 6 |   ) {
 7 | 
 8 |     _withUnprotectedUnsafeTemporaryAllocation(
 9 |       of: Int.self, capacity: source.count
10 |     ) { buffer in
   |       `- note: consumed here
11 |       let _ = source.startIndex
12 |     }

Indeed, I can replace the body with:

  @inlinable
  public init(
    _ source: borrowing some RandomAccessCollection<Int>
  ) {
    let buffer = UnsafeMutableBufferPointer<Int>.allocate(capacity: source.count)
    let _ = source.startIndex
  }

And that works (so it definitely seems that the closure capture is the problem), but of course I'd lose the stack allocation doing it this way :frowning:

Since the closure is non-escaping, I think it should be possible for it to capture a borrow. Does anybody know of a workaround for this? Or a reason why it is correct that the compiler disallows it?

I found a semi-workaround (or possibly a bug). Define a local closure variable.

struct Foo {

  @inlinable
  public init<T>(
    _ source: borrowing T
  ) where T: RandomAccessCollection<Int> {

    let process: (borrowing T) -> (UnsafeMutableBufferPointer<Int>) -> Void = {
        source in { buffer in
            let _ = source.startIndex
        }
    }

    _withUnprotectedUnsafeTemporaryAllocation(
      of: Int.self,
      capacity: source.count,
      process(source)
    )
  }
}

Don't ask me why this works - for some reason I can capture the borrow inside the return value of process :man_shrugging:. The generated code still has a retain/release pair, so I'm still not entirely getting the result I wanted.

EDIT: Oh, yeah, it just makes an implicit copy (which it shouldn't for a parameter with explicit borrowing annotation). So it's not a workaround, after all :slightly_frowning_face:.

func ok(_ source: borrowing [Int]) {
  // This is an error: 'source' is borrowed and cannot be consumed.
  _ = consume source
}

func huh(_ source: borrowing [Int]) {
  // Surely this should be an error, but it isn't.
  let process: (borrowing [Int]) -> Void = {
    s in _ = consume s
  }
  process(source)
}

Hmm... from the review on Noncopyable structs:

So I think the thing I want to do should be allowed (a nonescaping closure should borrow its capture, so it's fine that the captured value is itself also borrowed from the caller).

And from the review of borrow/take parameter modifiers:

(I don't see that there was any answer to that question, but I also think it's sensible thing to allow explicit ownership modifiers in capture lists).

The fact that captures are able to perform implicit copies seems like a straight bug, as it's possible to use it to crash the compiler (nightly):

struct Foo<T: ~Copyable>: ~Copyable {}

func huh<T: ~Copyable>(_ source: borrowing Foo<T>) {
  let process: (borrowing Foo<T>) -> () -> Void = {
    s in { _ = consume s }  // This should not be valid!
  }
  process(source)()
}

/swift-frontend: /home/build-user/swift/lib/SILGen/SILGenFunction.cpp:910: void swift::Lowering::SILGenFunction::emitCaptures(swift::SILLocation, swift::SILDeclRef, swift::Lowering::CaptureEmission, SmallVectorImpl<swift::Lowering::ManagedValue> &): Assertion `val->getType().isAddress() && "no address for captured var!"' failed.

Godbolt

For copyable types it does not crash (as shown in the previous post), but the implicit copy behaviour is actually less desirable than crashing IMO, because it means it would be a source-breaking change if we wanted to fix this and remove the implicit copying.

1 Like

Yeah, this is a bug. Your original code should be accepted. A less invasive workaround might be to copy the value at the point of capture:

struct Foo {
  @inlinable
  public init(
    _ source: borrowing some RandomAccessCollection<Int>
  ) {

    _withUnprotectedUnsafeTemporaryAllocation(
      of: Int.self, capacity: source.count
    ) { [source = copy source] buffer in
      let _ = source.startIndex
    }
  }
}```
1 Like