[Please skip to the bottom of the thread - the cause of the compiler crash is assigning an async value to a local lazy var]
I am working on an app which communicates between multiple network devices simultaneously, mainly using the Network and CoreBluetooth frameworks which are both pre-concurrency.
I have frequently encountered a situation in my async code where I need to suspend, waiting for some other piece of code to be in a certain state, and/or monitor state changes.
After lots of hair pulling and misdirection, I had settled on the following actor which I use in multiple places, to arbitrate such exchanges between contexts. It encapsulates the state value and an AsyncStream of those values as they change, but limits access to the stream to one context at a time to avoid the unwritten restriction that AsyncStreams cannot be iterated over by multiple contexts simultaneously. (As a side observation, I was surprised that I was able to create multiple iterators on the stream from different contexts without any runtime errors, but I found the behavior unpredictable).
actor AsyncState<State> where State: Equatable {
private(set) var value: State {
didSet {
if oldValue != value {
print("Scan State: \(String(describing: self.value))")
stateContinuation?.yield(value)
}
}
}
private lazy var stateUpdates: AsyncStream<State> = {
AsyncStream { (continuation: AsyncStream<State>.Continuation) -> Void in
self.stateContinuation = continuation
}
}()
private var stateContinuation: AsyncStream<State>.Continuation?
init(state: State) {
self.value = state
}
var streamSubscribed = false
func obtainSubscription() -> AsyncStream<State> {
if streamSubscribed { fatalError("Subscribing to AsyncState<\(State.Type.self)> stream multiple times")}
streamSubscribed = true
return stateUpdates
}
func relinquishSubscription() {
streamSubscribed = false
}
func change(to newState: State) {
let oldState = self.value
if oldState == newState { return }
print("Change State: from: \(oldState) to: \(newState)")
self.value = newState
}
}
Now this had been pretty successful until this morning when I managed to crash Xcode 15.3 with the following (simplified) code:
class Connection {
enum State {
case waitingToConnect, readyToPair, communicating, pairing, paired, unavailable, disconnected
}
enum MyError: Error {
case notConnected
}
private let state = AsyncState<State>(state: .waitingToConnect)
func waitTillPaired() async throws {
var state: State? = await self.state.value
// *** swift-frontend crashes on the following line 26 ***
lazy var stateChanges = await self.state.obtainSubscription().makeAsyncIterator()
defer {
Task {
await self.state.relinquishSubscription()
}
}
repeat {
guard state != .unavailable else { throw MyError.notConnected }
if state == .paired {
return
}
state = await stateChanges.next()
} while state != nil
throw MyError.notConnected
}
}
With just the above in a fresh project, Xcode crashes with the following:
1. Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4)
2. Compiling with the current language version
3. While evaluating request IRGenRequest(IR Generation for file ".../TestAsyncState/Connection.swift")
4. While emitting IR SIL function "@$s14TestAsyncState10ConnectionC14waitTillPaired12reachability7timeoutyAA0bC0CySbG_SdtYaKF12stateChangesL_ScS8IteratorVyAC0C0O_Gvg".
for getter for stateChanges (at .../TestAsyncState/Connection.swift:26:18)
I will report the error on Feedback Assistant, but am writing here to ask whether anyone thinks I am misusing the concurrency system, or if there is a better/cleaner/safer way to achieve the same outcome - i.e an ability to check, monitor or wait for a state change asynchronously.