hachyderm.io is one of the many independent Mastodon servers you can use to participate in the fediverse.
Hachyderm is a safe space, LGBTQIA+ and BLM, primarily comprised of tech industry professionals world wide. Note that many non-user account types have restrictions - please see our About page.

Administered by:

Server stats:

10K
active users

Anyone know why calling the following in a MainActor class/func

MyTest.increment(1) { result in
NSLog("result=\(result)")
}

crashes (asserts) when building with Swift 6?

I get that its not happy that the completion is coming in on another dispatch_queue but it should complain about it at compile time, or ignore it at run time.

@paul The block parameter is not marked with NS_SENDABLE, so the closure is expected to run in the same isolation domain as the call to increment (ie, the main actor). Objective-C isn’t fulfilling that contract, so Swift detects it at runtime deterministically (the trap) rather than let it become a data race.

@dgregor79 yeah I got it eventually figured out, just don't think this should crash vs doing something like logging a warning.

@paul @dgregor79 I think that’s a philosophical thing. The assert is to guard against quiet corruption of user data. It wouldn’t have compiled if they’d have been able to detect the case so a hard stop at runtime seems like the next best thing aligned with that. While this case is pretty harmless they can’t really know, especially at runtime, so it makes sense to be conservative about it. (Given the priorities of Swift, etc)

@Gte @dgregor79 I'm not a compiler engineer, nor a Swift expert but something in this behavior seems off to me. I run the code in Swift 5 it works fine, I run it in Swift 6 and it asserts (even though it compiles without any warnings).

I'd be fine if the default behavior was Swift expecting the block to return in whatever context, and then defines for always returns in main or returns in calling context (though no idea how to implement that one).

@paul @Gte The Objective-C code is calling the closure in an unexpected isolation context (I.e., a data race).The Swift compiler can’t diagnose that at compile time—it would have to be the Objective-C compiler, augmented with an understanding of Swift’s concurrency model.

In Swift 5 mode, this code silentlyintroduces a data race into the program.

In Swift 6 mode, the data race is caught by the dynamic isolation check. That’s the first point at which the data race can be detected, and the check is there to prevent this race from becoming weird runtime behavior.

This is all as designed. If that Objective-C code were Swift 6 code, we’d catch the error at compile time. As Objective-C, runtime is the earliest it’s possible to detect the race. Enabling Swift 6 language mode means turning previously-unobserved or undiagnosed data races into ones that fail predictably to flush out any contract violations outside of the Swift 6 code. As more code enables Swift 6, the runtime checks get replaced with compile-time.

Mike Apurin

@dgregor79 @paul @Gte I think that Swift 5 mode doing nothing is part of the problem here.
I've run in similar issues in Combine and was very blindsided by it. There is no way to progressively discover and deal with such isolation violations, just enabling 6 mode and praying. hachyderm.io/@auramagi/1131547