A limitation in RBI caused by closure isolation inference?

While trying something I found the following code didn't compile:

@MainActor
func test1() async {
    var a = 1
    await perform {
        a = 0 // note: sending main actor-isolated value of non-Sendable type '() -> ()' to nonisolated global function 'perform' risks causing races in between main actor-isolated and nonisolated uses
    }
}

@concurrent
func perform(_ fn: () -> Void) async {
    fn()
}

This surprised me because I had thought compiler was able to determine that a and the closure capturing it are in the same disconnected region and transfer them to global executor. This is how it works when there is no closure involved.

Example: it works when there is no closure involved

Compiler has no problem in transfering the disconnected region containing ns1 and NS2(ns1) to global executor:

class NS1 {}
class NS2 {
    let ns1: NS1
    init(_ ns1: NS1) {
        self.ns1 = ns1
    }
}

@MainActor
func test() async {
    let ns1 = NS1()
    await send(NS2(ns1))
}

@concurrent
func send(_ ns2: NS2) async {}

I found a workaround:

  @concurrent
- func perform(_ fn: () -> Void) async {
+ func perform(_ fn: sending () -> Void) async {
      fn()
  }

But IMO the workaround shouldn't be required, because sending parameter is intented to affect callee (perform), instead of caller (test).

Then I realized this was caused by closure isolation inference rules.

  • When perform(_:)'s closure parameter isn't marked sending, rule 1 in SE-0461 takes effective and the closure is inferred MainActor-isolated, hence the error.
  • When it's marked sending, rule 2 in SE-0461 takes effective and the closure is inferred nonisolated, hence no issue.

Hmm...I didn't expect there would still be such simple code I didn't fully understand.

Another workaround which doesn't require sending. It doesn't work for nonisolated functions yet because of the language's limitation. So the example uses two global actors.

@globalActor actor MyGlobalActor {
    static let shared = MyGlobalActor()
}

@MyGlobalActor
func test() async {
    var a = 1
    await perform { @MainActor in
        a = 0
    }
}

@MainActor
func perform(_ fn: @MainActor () -> Void) async {
    fn()
}