Missing cross-isolation diagnostics?

I just cannot understand the difference between:

class NonSendable {}

@MainActor func foo(_ a: NonSendable) async {
    await bar(a)    // cross-isolation
}

nonisolated func bar(_ a: NonSendable) async {}

and

class NonSendable {}

@MainActor func foo(_ a: NonSendable) async {
    Task { @MainActor in
        await bar(a)    // cross-isolation
    }
}

nonisolated func bar(_ a: NonSendable) async {}

The first one does not compile, while the second one does.
Is this difference coved by any RBI rules? @hborla @Michael_Gottesman

I believe that bar is eagerly ‘nonisolating’ itself by jumping to the preferred task executor. In the second case the compiler knows that the default task executor is MainActor (which matches the isolation of foo), whereas in the first case the default task executor isn’t necessarily MainActor (depends on the calling context of foo). I believe that this exact situation is one of many that SE-0461 seeks to address, after which both versions of the code should work.

Edit: Whoops, just realised Task { @MainActor in … } doesn’t set task executor preference (duh)

Both foo are already MainActor isolated, and both of the the calling contexts of bar(a) are MainActor too. If there's a data race safety hole in one case, that should be true for the other case.

Example 2 is apparently a bug. I suspect what happens in example 2 is that RBI analysis is performed in two steps in this case:

  • First, it notices task closure captures parameter a. Since the closure is in @MainActor isolation, it mistakenly thinks it's OK.
  • Then it performs the analysis on the closure body. Note that a is in task isolation region at this moment, so it's fine to send it to nonisolated asyn func.
1 Like

i agree the lack of diagnostics is a bug. IIUC, per the RBI rules both of these cases should produce isolation crossing diagnostics (assuming that one is not using the NonisolatedNonsendingByDefault configuration). the non-Sendable parameters of global actor-isolated functions should be considered within the same region as the actor, so should not be allowed to cross isolation boundaries unless they are explicitly marked sending. locally applying the patch from this outstanding PR returns symmetric behavior to the two examples (in addition to resolving a number of other long-standing reported issues).

Awesome, I’ll check that PR. Thanks for your info.

And btw, I agree with your analysis, any captured parameter of a global isolated closure should not be treated as task isolated.