speculating a bit, but i think this may be 'working as expected'. in the original RBI evolution doc i found this callout, which perhaps explains this behavior:
Within the body of a non-Sendable closure, the closure and its non-Sendable captures are treated as being Task isolated since just like a parameter, both the closure and the captures may have uses in their caller:
so i think the implication in this circumstance is that when operation
is captured by the withTaskGroup
closure, it is, from the vantage of RBI, 'task-isolated' and not in a disconnected region. this means when attempting to further pass it through to addTask
, it will be unable to be sent (hence the compiler error presumably).
you can seemingly work around this with a bit of indirection, e.g. the following appears to compile:
func send(
operation: sending @escaping () async -> Void
) async {
let box = { operation }
await withTaskGroup(of: Void.self) { taskGroup in
let operation = box()
taskGroup.addTask {
await operation()
}
}
}
i'm not entirely sure what the RBI rules that get applied in this case are, but my guess is that box
would be treated as disconnected per this comment:
A non-
Sendable
closure's region is the merge of its non-Sendable
captured parameters. As such a nonisolated non-Sendable
closure that only captures values that are in disconnected regions must itself be in a disconnected region and can be transferred
so it can be sent into the withTaskGroup
body. then when operation
is pulled back out within the inner closure, it's treated as still being in a disconnected region maybe? that step is where i'm least certain i understand which rules are being applied (or if the behavior is correct...).
anyway, hopefully that helps illuminate things somewhat. would be great to hear from the experts though cc @hborla @Michael_Gottesman.
edit: as @rayx points out below, the boxing/unboxing 'trick' may be a bug, as its use can also lead to subverting data race detection in some cases, so 'caveat programmer'.