Uh, this is tricky, but I think I know what's going on here.
By using isolation: isolated (any Actor)? = #isolation
, you're telling the compiler that runBlock
runs in the same isolation domain as the caller. However (and this is the tricky bit), the compiler doesn't know that block
should also run on the same isolation domain as the caller.
This is more or less the exact issue described as one of the motivation examples for Run nonisolated async functions in the caller actor by default (SE-0461). Another clue that this is the underlying issue is that annotating block
with the (underscored, not stable) attribute @_inheritActorContext
works:
func runBlock(
isolation: isolated (any Actor)? = #isolation,
@_inheritActorContext _ block: () async -> ()
) async {
await block()
}
So the compiler sees that runBlock
has no specific isolation requirements for its block
parameter, therefore it must do sendability checks for the closure. And then it detects that the closure you're passing to block
includes a non-sendable value.
The only way to pass that non-sendable value across isolation domains is by sending
it. But although sending
works in a function, for example in your main1
function:
@MainActor
func main1() async {
let ns = NonSendable()
await foo(ns)
await foo(ns)
}
It doesn't work for the closure. Generally speaking, sending
a value to a function as an argument is more reliable than creating a closure where that same value is sent to the function and passing that closure as an argument to a function.
That's because the function that receives the closure is allowed (depending on a combination of other factors, like whether the closure is @escaping
/async
/@Sendable
) to do weirder things with the closure, like calling it more than once or calling it multiple times in parallel which may mess up the logic of whether some value can be sent.