swiftlang / swift

The Swift Programming Language
https://swift.org
Apache License 2.0
67.58k stars 10.36k forks source link

[Concurrency] `async let` + `AsyncSequence` leads to "Pattern that the region based isolation checker does not understand" #75861

Open NachoSoto opened 2 months ago

NachoSoto commented 2 months ago

Reproduction

import Combine

@MainActor
final class Data {
  enum E {}

  @Published public private(set) var data: E?

  public var stream: AsyncPublisher<Published<E?>.Publisher> {
    $data.values
  }
}

actor Actor {
  private func f() async {
    // Pattern that the region based isolation checker does not understand how to check. Please file a bug
    async let stream = await data.stream
    for await _ in await stream {}
  }

  @MainActor private let data = Data()
}

Expected behavior

This compiles like it did on Swift 5 and Swift 6 beta 1.

Environment

swift-driver version: 1.113 Apple Swift version 6.0 (swiftlang-6.0.0.7.6 clang-1600.0.24.1)
Target: arm64-apple-macosx14.0

Additional information

Adding a local copy of data works around this:

let data = data
async let stream = await data.stream
NachoSoto commented 2 months ago

cc @hborla

NachoSoto commented 2 months ago

I tried creating a repro without Combine but this does not trigger it, presumively because AsyncStream is Sendable:

@MainActor
final class Data {
  public var stream: AsyncStream<Int> {
    .init { _ in }
  }
}

actor Actor {
  private func f() async {
    async let stream = await data.stream
    for await _ in await stream {}
  }

  @MainActor private let data = Data()
}
NachoSoto commented 2 months ago

Interestingly enough, this provides a proper error message:

@available(iOS 18.0, *)
@MainActor
final class Data {
  public var stream: some AsyncSequence<Int, Never> {
    AsyncStream { _ in }
  }
}

@available(iOS 18.0, *)
actor Actor {
  private func f() async {
    // Non-sendable type 'some AsyncSequence<Int, Never>' in implicitly asynchronous access to main actor-isolated property 'stream' cannot cross actor boundary
    async let stream = await data.stream
    for await _ in await stream {}
  }

  @MainActor private let data = Data()
}
gottesmm commented 2 months ago

I am not seeing this error with the latest 6.0:

import Combine

@MainActor
final class Data {
  enum E {}

  @Published public private(set) var data: E?

  public var stream: AsyncPublisher<Published<E?>.Publisher> {
    $data.values
  }
}

actor Actor {
  private func f() async {
    async let stream = await data.stream
    for await _ in await stream {}
  }

  @MainActor private let data = Data()
}

Output:

gottesmm@Michaels-MacBook-Pro-155 tmp % xcrun  -sdk macosx -toolchain swift  swiftc -swift-version 6 -c test.swift
test.swift:18:24: error: non-sendable type 'AsyncPublisher<Published<Data.E?>.Publisher>' returned by implicitly asynchronous call to nonisolated function cannot cross actor boundary
16 |   private func f() async {
17 |     // Pattern that the region based isolation checker does not understand how to check. Please file a bug
18 |     async let stream = await data.stream
   |                        `- error: non-sendable type 'AsyncPublisher<Published<Data.E?>.Publisher>' returned by implicitly asynchronous call to nonisolated function cannot cross actor boundary
19 |     for await _ in await stream {}
20 |   }

Combine.AsyncPublisher:2:15: note: generic struct 'AsyncPublisher' does not conform to the 'Sendable' protocol
 1 | @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
 2 | public struct AsyncPublisher<P> : AsyncSequence where P : Publisher, P.Failure == Never {
   |               `- note: generic struct 'AsyncPublisher' does not conform to the 'Sendable' protocol
 3 |     public typealias Element = P.Output
 4 |     public struct Iterator : AsyncIteratorProtocol {

test.swift:2:1: warning: add '@preconcurrency' to treat 'Sendable'-related errors from module 'Combine' as warnings
 1 | 
 2 | import Combine
   | `- warning: add '@preconcurrency' to treat 'Sendable'-related errors from module 'Combine' as warnings
 3 | 
 4 | @MainActor

test.swift:18:35: error: non-sendable type 'AsyncPublisher<Published<Data.E?>.Publisher>' in implicitly asynchronous access to main actor-isolated property 'stream' cannot cross actor boundary
16 |   private func f() async {
17 |     // Pattern that the region based isolation checker does not understand how to check. Please file a bug
18 |     async let stream = await data.stream
   |                                   `- error: non-sendable type 'AsyncPublisher<Published<Data.E?>.Publisher>' in implicitly asynchronous access to main actor-isolated property 'stream' cannot cross actor boundary
19 |     for await _ in await stream {}
20 |   }

Combine.AsyncPublisher:2:15: note: generic struct 'AsyncPublisher' does not conform to the 'Sendable' protocol
 1 | @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
 2 | public struct AsyncPublisher<P> : AsyncSequence where P : Publisher, P.Failure == Never {
   |               `- note: generic struct 'AsyncPublisher' does not conform to the 'Sendable' protocol
 3 |     public typealias Element = P.Output
 4 |     public struct Iterator : AsyncIteratorProtocol {
NachoSoto commented 2 months ago

Cool so I guess it can produce the error now. But I think it should still not fail? If you replace it with

let data = data
async let stream = await data.stream

Does it work?

gottesmm commented 3 weeks ago

No. What is happening here is that the async let in stream happens in a MainActor isolated context. The async let is actually nonisolated. So MainActor isolated state is being exposed to async let work.

That being said, I could imagine something like this:

@MainActor async let stream = await data.stream
gottesmm commented 3 weeks ago

but we don't have that today.