Unfortunately, the compiler already implicitly surfaces region information and produces region-based errors, so it's too late. @FranzBusch already covered the general why, but to give a more precise example (from PointFree's recent videos on this very subject).
func regions() {
let first = Account()
let second = Account()
takeBoth(first, second)
Task {
first.balance += 5
}
second.balance += 10
}
final class Account {
var balance = 0
}
func takeBoth(_ first: Account, _ second: Account) {
print(first, second)
}
The diagnostics produced in this code are ultimately accurate, given the current limitations of region analysis, but are actively misleading.
First, it highlights the Task init and produces:
Sending value of non-Sendable type '() async -> ()' risks causing data races
This is rather confusing as it is, since the base error message doesn't include the name of the value it thinks I'm sending, and even assuming it's talking about first, is possibly confusing with my other, similar usage, where passing a reference type to a Task works just fine.
In Xcode and the command line you can get a bit more info:
Tester.swift:19:5: error: sending value of non-Sendable type '() async -> ()' risks causing data races
Task {
^~~~~~
Tester.swift:19:5: note: Passing value of non-Sendable type '() async -> ()' as a 'sending' argument to initializer 'init(name:priority:operation:)' risks causing races in between local and caller code
Task {
^
Tester.swift:23:5: note: access can happen concurrently
second.balance += 10
But this is even more confusing, as somehow the compiler thinks I'm sending second to the Task?
Xcode's "Show" visualization makes this even worse. (Pardon the screen shot.)
It highlights
second, instead of
first, which is actually captured by the
Task that produces the error. What the heck is going on?
Thankfully, the latest nightly has better diagnostics, though still not quite right. (via Godbolt)
10 |
11 | Task {
12 | first.balance += 5
| `- error: closure passed as an argument to a 'sending' parameter captures 'first' which is accessed later by code in the current task [#RegionIsolation::SendingRisksDataRace]
13 | }
14 |
15 | second.balance += 10
| `- note: access can happen concurrently
16 | }
17 |
[#RegionIsolation]: <https://docs.swift.org/compiler/documentation/diagnostics/region-isolation>
[#SendingRisksDataRace]: <https://docs.swift.org/compiler/documentation/diagnostics/sending-risks-data-race>
Even this new diagnostic isn't right. The message on first says it's "accessed later by code in the current task", but that's clearly not true, even in diagnostic itself. Instead, it later points to the use of second and says "access can happen concurrently", which also isn't true, as second is only accessed outside the Task. But at least there are some links to region isolation that might help point to the real issue.
For those more familiar with region based isolation and its various rules, the actual issue in this code, which is never mentioned in any of the diagnostics, is that takeBoth(first, second) merges the isolation regions of first and second, making subsequent sending captures of one act like both were sent instead. The critical notion of region merging is never mentioned in the diagnostics! (Unfortunately there doesn't seem to be a great way, even in the nightlies, to tell the compiler this is safe.)
So not only is region isolation already exposed by the compiler, we need to expose it more to make errors clear and help the user come up with different solutions.