Potential issue in rethrowing protocol

Hi,

There might be an issue in the current implementation of rethrowing protocol conformance.
From what I understand, a generic type that conforms to a rethrowing protocol can rethrow only if it uses the generic parameter as a try statement. For instance:

struct AsyncDummySequence<Base: AsyncSequence>: AsyncSequence {
  typealias Element = Base.Element
  typealias AsyncIterator = Iterator

let base: Base

  func makeAsyncIterator() -> AsyncIterator {
    Iterator(base: self.base.makeAsyncIterator())
  }

  struct Iterator: AsyncIteratorProtocol {
    var base: Base.AsyncIterator

    mutating func next() async rethrows -> Element? {
      try await self.base.next()
    }
}

The next() function is allowed to rethrow because it uses the generic parameter.

let nonThrowingSequence = AsyncStream<Int> { continuation in
  continuation.yield(1)
  continuation.finish()
}

let throwingSequence = AsyncThrowingStream<Int, Error> { continuation in
  continuation.yield(1)
  continuation.finish()
}

let seq1 = AsyncDummySequence(base: nonThrowingSequence)
for await element in seq1 { // -> no need to use `try await`
  print(element)
}

let seq2 = AsyncDummySequence(base: throwingSequence)
for try await element in seq2 { // -> need to use `try await`
  print(element)
}

So far so good. When one want to use a try statement not related to a generic parameter, the compiler will complain.

mutating func next() async rethrows -> Element? {
  try Result<Element, Error>.failure(NSError(domain: "", code: 1)).get() // -> does not compile
}

This is the expected behaviour. But there is a way to bypass the compiler checking by wrapping the call in a rethrowing function, like withTaskCancellationHandler() for instance (works with any rethrowing function).

mutating func next() async rethrows -> Element? {
  try await withTaskCancellationHandler {
  } operation: {
    try Result<Element, Error>.failure(NSError(domain: "", code: 1)).get() // -> does compile
  }
}

we can now misuse the AsyncDummySequence:

let nonThrowingSequence = AsyncStream<Int> { continuation in
  continuation.yield(1)
  continuation.finish()
}

let seq = AsyncDummySequence(base: nonThrowingSequence)

for await element in seq { // -> no need to use `try await` although the sequence will throw
  print(element)
}

When running that code, the iteration seems to be blocked.

I guess this is a known issue that is also related to a potential rethrow(unsafe) mechanism ? I wanted to ask the question though because this is something that can already be misused in the released version of Swift.

@Douglas_Gregor or @Philippe_Hausler might be the best persons to answer that ?

Thanks a lot and have a nice day.

That definitely looks like a bug to me. My guess is that we need to somehow determine if the rethrow of the closure is resolved or not.

Thanks for your answer @Philippe_Hausler. Where should I submit that issue ?

That is a compiler issue and should be submitted to GitHub.

Will do.

Thanks.

1 Like

For the record: Issue in rethrowing protocol · Issue #60821 · apple/swift · GitHub