I'm seeing a crash compiling with Swift 6 that I can reproduce with the following code.
It crashes with "Incorrect actor executor assumption". Is there something that the compiler should be warning about so that this isn't a runtime crash?
Note - if I use a for in
loop instead of the .forEach
closure, the crash does not happen.
Is the compiler somehow inferring the wrong isolation domain for the closure?
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.task {
_ = try? await MyActor(store: MyStore())
}
}
}
actor MyActor {
var credentials = [String]()
init(store: MyStore) async throws {
try await store.persisted.forEach {
credentials.append($0)
}
}
}
final class MyStore: Sendable {
var persisted: [String] {
get async throws {
return ["abc"]
}
}
}
The stack trace is:
* thread #6, queue = 'com.apple.root.user-initiated-qos.cooperative', stop reason = signal SIGABRT
frame #0: 0x0000000101988f30 libsystem_kernel.dylib`__pthread_kill + 8
frame #1: 0x0000000100e2f124 libsystem_pthread.dylib`pthread_kill + 256
frame #2: 0x000000018016c4ec libsystem_c.dylib`abort + 104
frame #3: 0x00000002444c944c libswift_Concurrency.dylib`swift::swift_Concurrency_fatalErrorv(unsigned int, char const*, char*) + 28
frame #4: 0x00000002444c9468 libswift_Concurrency.dylib`swift::swift_Concurrency_fatalError(unsigned int, char const*, ...) + 28
frame #5: 0x00000002444c90e0 libswift_Concurrency.dylib`swift_task_checkIsolated + 152
frame #6: 0x00000002444c63e0 libswift_Concurrency.dylib`swift_task_isCurrentExecutorImpl(swift::SerialExecutorRef) + 284
frame #7: 0x0000000100d58944 IncorrectActorExecutorAssumption.debug.dylib`closure #1 in MyActor.init($0="abc") at <stdin>:0
frame #8: 0x0000000100d58b94 IncorrectActorExecutorAssumption.debug.dylib`partial apply for closure #1 in MyActor.init(store:) at <compiler-generated>:0
frame #9: 0x00000001947f8c80 libswiftCore.dylib`Swift.Sequence.forEach((τ_0_0.Element) throws -> ()) throws -> () + 428
* frame #10: 0x0000000100d58748 IncorrectActorExecutorAssumption.debug.dylib`MyActor.init(store=0x0000600000010ba0) at ContentView.swift:27:35
frame #11: 0x0000000100d57734 IncorrectActorExecutorAssumption.debug.dylib`closure #1 in ContentView.body.getter at ContentView.swift:14:32
frame #12: 0x0000000100d57734 IncorrectActorExecutorAssumption.debug.dylib`closure #1 in ContentView.body.getter at ContentView.swift:14:32
frame #13: 0x00000001d1817138 SwiftUI`(1) await resume partial function for partial apply forwarder for closure #1 () async -> () in closure #1 (inout Swift.TaskGroup<()>) async -> () in closure #1 () async -> () in SwiftUI.AppDelegate.application(_: __C.UIApplication, handleEventsForBackgroundURLSession: Swift.String, completionHandler: () -> ()) -> ()
frame #14: 0x00000001d17b1e48 SwiftUI`(1) await resume partial function for dispatch thunk of static SwiftUI.PreviewModifier.makeSharedContext() async throws -> τ_0_0.Context
frame #15: 0x00000001d19c10c0 SwiftUI`(1) await resume partial function for generic specialization <()> of reabstraction thunk helper <τ_0_0 where τ_0_0: Swift.Sendable> from @escaping @isolated(any) @callee_guaranteed @async () -> (@out τ_0_0) to @escaping @callee_guaranteed @async () -> (@out τ_0_0, @error @owned Swift.Error)
frame #16: 0x00000001d17b1e48 SwiftUI`(1) await resume partial function for dispatch thunk of static SwiftUI.PreviewModifier.makeSharedContext() async throws -> τ_0_0.Context
Weirdly, I don’t see this trap, but I do get a runtime issue:
try await store.persisted.forEach {
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// warning: data race detected: actor-isolated function at … was not called on the same actor
I’m able to avoid that by rewrite the code like this:
actor MyActor {
let store: MyStore
var credentials = [String]()
init(store: MyStore) async throws {
self.store = store
try await self.test()
}
func test() async throws {
try await store.persisted.forEach {
credentials.append($0)
}
}
}
I think this issue relates to the fact that you’re doing this in an initialiser. Initialises have two phases: pre and post complete initialisation. After complete initialisation the actor is fully up and running, and thus you can do things like call async methods. Before initialisation the actor is kinda in a bit of a limbo state. For example, if you reversed the two lines in my initialiser above, the compiler complains that Variable 'self.store' used before being initialized
, which makes perfect sense.
I think what’s happening in your case is that the compiler hasn’t realised that the actor is fully initialised and thus it’s applying its pre-initialisation checks. However, the actor is fully initialised, and so you’re expecting it to apply its post-initialisation checks.
I’m able to avoid this problem by moving the call off to an async method. Calling that in the initialiser convinces the compiler to use its post-initialisation checks.
Still, I’m not a compiler engineer, so my rationale is largely speculative. However, the path forward is clear: It would be nice if the compiler either did the right thing here or issued a compile-time error. Discovering this at runtime isn’t any fun. I encourage you to file a bug against Swift itself with this example.
Please post your bug number, just for the record.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"